diff --git a/README.md b/README.md index 4c371f6..3c33847 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Red proof must fail before implementation. Green proof must pass after implement | Surface | OpenFlow Position | | --- | --- | | Goal alignment | Project-level `.xflow/GOAL.md` that yolo, corps, Ralph, handoff, and takein consume | -| One-shot evaluation | `xflow evaluate` combines quality score, Codex goal / Superpowers comparison, ordinary and splash launch blockers, release-owner status, claim boundaries, next actions, and first-run path | +| One-shot evaluation | `xflow evaluate` combines quality score, Codex goal / Superpowers / Super-Assistant comparison, ordinary and splash launch blockers, release-owner status, claim boundaries, next actions, and first-run path | | Quality assessment | `xflow assess` gives a public scorecard for goal, yolo, corps, and the skill family | | Launch demo | `xflow demo launch` shows goal -> yolo and goal -> corps proof paths | | Launch audit | `xflow launch audit` separates ready engine and adoption evidence from missing registry proof | @@ -119,7 +119,7 @@ Red proof must fail before implementation. Green proof must pass after implement | Team handoff | `HANDOFF.md`, `AHA.md`, reviewer guide | | Team adoption | Role model, CI boundary, PR acceptance, release ownership | | Cross-tool use | Plain CLI and skill docs for Codex, Claude Code, Cursor, OpenCode, Gemini | -| Competitive proof | `xflow compare codex-goal`, `xflow compare superpowers`, `xflow compare openspec`, `xflow compare gstack`, `xflow compare spec-kit`, benchmark docs | +| Competitive proof | `xflow compare codex-goal`, `xflow compare superpowers`, `xflow compare super-assistant`, `xflow compare openspec`, `xflow compare gstack`, `xflow compare spec-kit`, benchmark docs | | Adoption proof | Minimal clean project runs in CI, plus public adoption-record templates | ## Docs @@ -136,6 +136,7 @@ Red proof must fail before implementation. Green proof must pass after implement - [Launch dossier](docs/launch-dossier.md) - [Corps operator guide](docs/corps-operator-guide.md) - [Superpowers comparison](docs/superpowers-comparison.md) +- [Super-Assistant comparison](docs/super-assistant-comparison.md) - [Adoption evidence template](docs/adoption/README.md) - [Maintainer adoption record](docs/adoption/openflow-release-hardening.md) - [xflow Goal vs Codex native goal](docs/goal-vs-codex.md) diff --git a/atoms/registry.json b/atoms/registry.json index 1232f2a..4519810 100644 --- a/atoms/registry.json +++ b/atoms/registry.json @@ -621,6 +621,62 @@ "description": "Walk specs/changes// and phase-run artifactRefs, build evidence packet for gate", "requires_http": false }, + "P1.policy.resolve": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/policy/resolve.js", + "export": "policyResolve", + "description": "Resolve personal/team policy overlay and write policy_effective.json without forking yolo/corps", + "requires_http": false + }, + "P2.policy.gate": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/policy/gate.js", + "export": "policyGate", + "description": "Evaluate effective team policy against coverage, reviewer, and smoke evidence; write structured policy_gate.json", + "requires_http": false + }, + "G5.intake.recommend": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/intake/recommend.js", + "export": "intakeRecommend", + "description": "Build demand profile and workflow_recommendation.json for yolo/corps plus optional gate overlays", + "requires_http": false + }, + "N1.notify.checkpoint": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/notify/checkpoint.js", + "export": "notifyCheckpoint", + "description": "Send file/mock checkpoint notification evidence; does not approve human gates", + "requires_http": false + }, + "N2.plugin.run": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/plugin/run.js", + "export": "pluginRun", + "description": "Run or dry-run generic script/webhook plugin providers and write structured plugin result evidence", + "requires_http": false + }, + "N3.storage.sync": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/storage/sync.js", + "export": "storageSync", + "description": "Import/export file/mock shared storage artifacts under .xflow/storage and record sync evidence", + "requires_http": false + }, + "N4.experience.digest": { + "track": "lite", + "type": "js", + "module": "src/core/atoms/experience/digest.js", + "export": "experienceDigest", + "description": "Summarize local/team experience entries into advisory experience_digest.json", + "requires_http": false + }, "K1.artifacts.complete_check": { "track": "lite", "type": "python", diff --git a/bin/xflow.js b/bin/xflow.js index f9d0e7d..089bdbf 100755 --- a/bin/xflow.js +++ b/bin/xflow.js @@ -51,6 +51,13 @@ import { buildWorkflowManifest, validateExecutionLogChain, } from '../src/core/workflow-integrity.js'; +import { + readProfile, + resolveEffectivePolicy, + validateTeamPolicy, + writeProfile, +} from '../src/core/policy-overlay.js'; +import { writeWorkflowRecommendation } from '../src/core/intake-recommendation.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..'); @@ -91,6 +98,15 @@ async function main() { case 'goal': await handleGoal(subcommand, rest); break; + case 'profile': + await handleProfile(subcommand, rest); + break; + case 'policy': + await handlePolicy(subcommand, rest); + break; + case 'intake': + await handleIntake(subcommand, rest); + break; case 'score': await handleScore([subcommand, ...rest].filter(Boolean)); break; @@ -164,6 +180,136 @@ async function main() { } } +// ─── profile / policy / intake ─────────────────────────────────────────────── + +async function handleProfile(sub, args) { + if (isHelpRequest(sub) || args.some(isHelpRequest)) { + printProfileHelp(); + return; + } + const parsedArgs = parseCliArgs(args); + const projectRoot = parsedArgs.project_root || process.env.XFLOW_PROJECT_ROOT || process.cwd(); + switch (sub) { + case 'show': { + const profile = readProfile(projectRoot); + const payload = { ok: true, project_root: projectRoot, profile }; + if (parsedArgs.json) { + console.log(JSON.stringify(payload, null, 2)); + } else { + console.log(`execution_mode: ${profile.execution_mode || 'personal'}`); + console.log(`workflow_tracks: personal=${profile.workflow_tracks?.personal || 'yolo|corps'}, team=${profile.workflow_tracks?.team || 'yolo|corps'}`); + } + break; + } + case 'set': { + const patch = {}; + if (parsedArgs.execution_mode) patch.execution_mode = parsedArgs.execution_mode; + if (parsedArgs.notify_provider) patch.notify = { provider: parsedArgs.notify_provider }; + if (parsedArgs.storage_provider) patch.storage = { provider: parsedArgs.storage_provider }; + const profile = writeProfile(projectRoot, patch); + const payload = { ok: true, project_root: projectRoot, profile }; + if (parsedArgs.json) { + console.log(JSON.stringify(payload, null, 2)); + } else { + console.log(`✓ profile updated: execution_mode=${profile.execution_mode || 'personal'}`); + } + break; + } + default: + console.error(`Unknown profile subcommand: ${sub}`); + console.error('Available: show, set'); + process.exit(1); + } +} + +async function handlePolicy(sub, args) { + if (isHelpRequest(sub) || args.some(isHelpRequest)) { + printPolicyHelp(); + return; + } + const parsedArgs = parseCliArgs(args); + const projectRoot = parsedArgs.project_root || process.env.XFLOW_PROJECT_ROOT || process.cwd(); + switch (sub) { + case 'validate': { + const result = resolveEffectivePolicy({ + projectRoot, + changeId: parsedArgs.change_id || process.env.CHANGE_ID || null, + executionMode: parsedArgs.execution_mode || null, + }); + const validation = result.team_policy ? validateTeamPolicy(result.team_policy) : { ok: true, errors: [] }; + const payload = { ...result, validation }; + if (parsedArgs.json) { + console.log(JSON.stringify(payload, null, 2)); + } else if (payload.ok) { + console.log(`✓ policy valid: execution_mode=${payload.policy.execution_mode}`); + } else { + console.error(`✗ policy invalid: ${payload.error?.message || 'unknown policy error'}`); + } + process.exit(payload.ok ? 0 : 1); + break; + } + default: + console.error(`Unknown policy subcommand: ${sub}`); + console.error('Available: validate'); + process.exit(1); + } +} + +async function handleIntake(sub, args) { + if (isHelpRequest(sub) || args.some(isHelpRequest)) { + printIntakeHelp(); + return; + } + const parsedArgs = parseCliArgs(args); + const projectRoot = parsedArgs.project_root || process.env.XFLOW_PROJECT_ROOT || process.cwd(); + switch (sub) { + case 'recommend': { + const changeId = parsedArgs.change_id || process.env.CHANGE_ID || deriveChangeIdFromBranch(currentBranch(projectRoot)); + if (!changeId) { + console.error('Usage: xflow intake recommend --change-id --title [--description <text>]'); + process.exit(1); + } + const policyResult = resolveEffectivePolicy({ + projectRoot, + changeId, + executionMode: parsedArgs.execution_mode || null, + }); + const recommendationInput = { + title: parsedArgs.title || '', + description: parsedArgs.description || '', + change_type: parsedArgs.change_type || parsedArgs.demand_type || '', + demand_type: parsedArgs.demand_type || '', + repo_count: parsedArgs.repo_count, + repositories: parsedArgs.repositories, + estimated_file_count: parsedArgs.estimated_file_count, + }; + const result = writeWorkflowRecommendation({ + projectRoot, + changeId, + input: recommendationInput, + effectivePolicy: policyResult.policy || policyResult.profile || {}, + }); + const payload = { + ok: true, + policy_ok: policyResult.ok, + policy_error: policyResult.error || null, + ...result, + }; + if (parsedArgs.json) { + console.log(JSON.stringify(payload, null, 2)); + } else { + console.log(`recommended_track: ${result.recommendation.recommended_track}`); + console.log(`workflow_recommendation_file: ${result.workflow_recommendation_file}`); + } + break; + } + default: + console.error(`Unknown intake subcommand: ${sub}`); + console.error('Available: recommend'); + process.exit(1); + } +} + // ─── adapter ───────────────────────────────────────────────────────────────── async function handleAdapter(sub, args) { @@ -627,6 +773,123 @@ const COMPARE_TARGETS = { ], next_proof: 'Use xflow launch dossier, xflow score, xflow workflow validate yolo/corps, and npm run release:pack; compare that machine evidence against the lighter Superpowers skill-driven operating model.', }, + 'super-assistant': { + target: 'super-assistant', + verdict: 'Super-Assistant is stronger as a broad enterprise harness with many concrete connectors; xflow is stronger after the policy-overlay integration because team constraints, intake routing, notifications, plugins, storage sync, and experience reuse are repo-local atoms with structured proof instead of a monolithic hook state machine.', + winner: { + overall: 'xflow', + basis: 'policy overlay on existing yolo/corps tracks, atomized enterprise integrations, structured gate failures, and repeatable local verification', + caveat: 'Super-Assistant still has broader concrete enterprise connector coverage; xflow intentionally absorbs those ideas as provider seams, not built-in secrets or workflow forks.', + confidence: 'high', + }, + scorecard: { + scale: '0-100', + weights: { + workflow_runtime_authority: 15, + policy_overlay_and_team_constraints: 15, + adaptive_intake: 10, + enterprise_atomization: 15, + notification_gate_separation: 10, + experience_reuse: 10, + verification_and_packaging: 15, + portability_and_secret_boundary: 10, + }, + overall: { + xflow: 95, + 'super-assistant': 76, + }, + dimensions: [ + { + id: 'workflow_runtime_authority', + weight: 15, + xflow: 15, + 'super-assistant': 10, + winner: 'xflow', + evidence_refs: ['workflows/yolo.yaml', 'workflows/corps.yaml', 'schemas/workflow.schema.json', 'atoms/registry.json', 'Super-Assistant harness-manifest.json and stage-completed hooks'], + }, + { + id: 'policy_overlay_and_team_constraints', + weight: 15, + xflow: 15, + 'super-assistant': 12, + winner: 'xflow', + evidence_refs: ['src/core/policy-overlay.js', '.xflow/profile.json', '.xflow/team-policy.json', 'test/policy-overlay.test.js', 'Super-Assistant lib/team_constraints.py'], + }, + { + id: 'adaptive_intake', + weight: 10, + xflow: 10, + 'super-assistant': 8, + winner: 'xflow', + evidence_refs: ['src/core/intake-recommendation.js', 'G5.intake.recommend', 'workflow_recommendation.json', 'Super-Assistant planner/adaptive routing references'], + }, + { + id: 'enterprise_atomization', + weight: 15, + xflow: 14, + 'super-assistant': 11, + winner: 'xflow', + evidence_refs: ['N1.notify.checkpoint', 'N2.plugin.run', 'N3.storage.sync', 'N4.experience.digest', 'src/core/atoms/*', 'Super-Assistant lib/plugin-extension/plugin_executor.py and lib/s3_sync'], + }, + { + id: 'notification_gate_separation', + weight: 10, + xflow: 10, + 'super-assistant': 6, + winner: 'xflow', + evidence_refs: ['src/core/atoms/notify/checkpoint.js approves_gate=false', 'P2.policy.gate structured failed_checks', 'Super-Assistant lib/checkpoint_manager.py'], + }, + { + id: 'experience_reuse', + weight: 10, + xflow: 9, + 'super-assistant': 8, + winner: 'xflow', + evidence_refs: ['src/core/atoms/experience/digest.js', 'experience_digest.json advisory=true', 'Super-Assistant lib/experience_index.py'], + }, + { + id: 'verification_and_packaging', + weight: 15, + xflow: 14, + 'super-assistant': 9, + winner: 'xflow', + evidence_refs: ['npm run verify', 'npm run drift:scan', 'xflow workflow validate yolo', 'xflow workflow validate corps', 'xflow score --json', 'Super-Assistant npm run check and unittest surfaces'], + }, + { + id: 'portability_and_secret_boundary', + weight: 10, + xflow: 8, + 'super-assistant': 5, + winner: 'xflow', + evidence_refs: ['file/mock providers only in first version', 'no DingTalk or S3 secrets in core', 'docs/super-assistant-comparison.md', 'Super-Assistant concrete DingTalk/S3 connector assumptions'], + }, + ], + }, + docs: ['docs/super-assistant-comparison.md', 'docs/competitive-benchmark.md', 'docs/integrations.md'], + xflow_edges: [ + 'personal/team execution mode is a policy overlay on yolo/corps instead of a workflow fork', + 'policy_effective.json makes merged personal profile and team policy reviewable', + 'workflow_recommendation.json turns demand profile into advisory track and gate input', + 'notify.checkpoint proves notification only and never substitutes for human ack', + 'plugin.run, storage.sync, and experience.digest are provider atoms with structured artifacts', + 'team policy gate returns machine-readable failed_checks instead of relying on hook logs', + ], + target_edges: [ + 'broad enterprise harness with many agents, hooks, and concrete platform integrations', + 'valuable team_constraints.py merge idea for coding/testing/review/deploy profiles', + 'checkpoint manager, plugin executor, experience index, and S3 sync contain useful implementation patterns', + 'good source of enterprise integration cases, but more coupled to harness state and platform-specific secrets', + ], + next_proof: 'Run xflow compare super-assistant --json, xflow policy validate --json, xflow intake recommend --change-id team-api-feature --title "team API feature" --description "touches API DB user data and CI" --repo-count 2 --json, xflow workflow validate yolo/corps, npm run verify, npm run drift:scan, and compare with Super-Assistant npm run check plus Python unittest output.', + }, + superassistant: { + target: 'super-assistant', + alias_for: 'super-assistant', + }, + 'super-assitant': { + target: 'super-assistant', + alias_for: 'super-assistant', + }, 'codex-goal': { target: 'codex-goal', verdict: 'Codex native goal is better for thread-local intent and completion accounting; xflow goal is better when the objective must become repo-owned alignment evidence consumed by yolo, corps, Ralph, handoff, and takein.', @@ -930,7 +1193,8 @@ function printAssessText(payload) { } function buildComparePayload(target) { - const payload = COMPARE_TARGETS[target]; + const initial = COMPARE_TARGETS[target]; + const payload = initial?.alias_for ? COMPARE_TARGETS[initial.alias_for] : initial; if (!payload) throw new Error(`Unknown compare target: ${target}`); const result = { ok: true, @@ -959,6 +1223,7 @@ async function handleEvaluate(args) { const competitive = { codex_goal: buildComparePayload('codex-goal'), superpowers: buildComparePayload('superpowers'), + super_assistant: buildComparePayload('super-assistant'), }; const withoutSelfEvaluate = (section) => ({ ...section, @@ -1020,6 +1285,7 @@ async function handleEvaluate(args) { 'xflow release status --json', 'xflow compare codex-goal --json', 'xflow compare superpowers --json', + 'xflow compare super-assistant --json', ...assess.proof_commands, 'xflow adoption status --json', 'xflow package status --json', @@ -6586,6 +6852,7 @@ Options: function printCompareHelp() { console.log(` Usage: xflow compare <superpowers|codex-goal|openspec|gstack|spec-kit> [--json] + xflow compare super-assistant [--json] Show a compact evidence-backed comparison against a reference spec workflow or agent-methodology system and point to the local proof docs/operators should @@ -6818,6 +7085,10 @@ Usage: xflow goal set "<goal>" Write the project-level .xflow/GOAL.md anchor xflow goal show [--json] Show the current project goal xflow goal audit [--json] Audit goal consumption across the skill family + xflow profile show [--json] Show personal/team execution profile + xflow profile set Set execution mode or provider preferences + xflow policy validate Validate effective policy overlay + xflow intake recommend Recommend yolo/corps and gate overlays xflow evaluate [--json] Show one-shot external evaluator brief xflow release status [--json] Show release-owner publish status xflow assess [--json] Show the public quality assessment @@ -6833,6 +7104,7 @@ Usage: xflow objective [--json] Audit the top-level product objective xflow compare codex-goal Compare xflow goal against Codex native goal xflow compare superpowers Compare xflow against Superpowers + xflow compare super-assistant Compare xflow against Super-Assistant xflow compare openspec Compare xflow against OpenSpec xflow compare gstack Compare xflow against gstack xflow compare spec-kit Compare xflow against spec-kit @@ -6870,6 +7142,8 @@ Quick start: xflow quickstart xflow goal set "Ship the next verified change" --project-root . xflow spec start --title "修复示例" --change-type backend + xflow profile show --json + xflow intake recommend --title "修复示例" --change-id example --json xflow workflow run workflows/yolo.yaml --dry-run xflow workflow run workflows/yolo.yaml --title "修复示例" --change-type backend xflow qa capture --url http://127.0.0.1:3000 --platform-profile mobile_h5 @@ -6885,6 +7159,34 @@ Quick start (corps): `); } +function printProfileHelp() { + console.log(` +xflow profile — manage personal/team execution profile. + +Usage: + xflow profile show [--project-root .] [--json] + xflow profile set --execution-mode personal|team [--project-root .] [--json] +`); +} + +function printPolicyHelp() { + console.log(` +xflow policy — validate policy overlay. + +Usage: + xflow policy validate [--project-root .] [--change-id <id>] [--execution-mode personal|team] [--json] +`); +} + +function printIntakeHelp() { + console.log(` +xflow intake — recommend workflow track and gate overlays from a demand profile. + +Usage: + xflow intake recommend --change-id <id> --title <title> [--description <text>] [--repo-count <n>] [--json] +`); +} + main().catch(e => { console.error(e.message); process.exit(1); diff --git a/docs/competitive-benchmark.md b/docs/competitive-benchmark.md index cb62904..a3f01ca 100644 --- a/docs/competitive-benchmark.md +++ b/docs/competitive-benchmark.md @@ -11,6 +11,7 @@ This benchmark is intentionally stricter than a marketing comparison. A category ## Reference Systems - Superpowers: strong skill distribution, agent behavior shaping, and test-driven coding guidance. +- Super-Assistant: broad enterprise harness with team constraints, checkpoint notifications, plugin execution, experience indexing, and S3-backed sync. - OpenSpec: strong spec-driven workflow, low-friction command surface, and cross-tool positioning. - gstack: strong role-specialist command surface, browser QA, and host-native release rituals for Codex / Claude Code. - spec-kit: strong specification-driven development lifecycle, broad public extension surface, and ecosystem credibility. @@ -25,7 +26,7 @@ This benchmark is intentionally stricter than a marketing comparison. A category | Verification rigor | Drift checks, tests, gates, and sync checks catch regressions before release. | `npm test`, `npm run drift:scan`, `npm run skill:diff`, deterministic gates | Strong | | TDD / code quality push | Red runs before implementation, green runs after implementation, and changed tests are checked for quality anti-patterns. | `I6a.tdd.run`, `I6b.tdd.proof_validate`, `I6c.tdd.quality_review`, `test/tdd-proof.test.js`, `verify_proof.json` | Strong | | Operator onboarding | First commands explain the human loop, initialize a project, and report whether the local install is ready. | `xflow quickstart`, `xflow guide`, `xflow init`, `xflow doctor`, `npm run doctor` | Strong | -| One-shot evaluation | External evaluators can see quality score, Codex goal / Superpowers positioning, plus OpenSpec / gstack comparisons, launch blockers, release-owner status, claim boundaries, next actions, and first-run path from one command family. | `xflow evaluate`, `xflow evaluate --json`, `xflow release status`, `xflow release status --json`, `xflow compare codex-goal --json`, `xflow compare superpowers --json`, `xflow compare openspec --json`, `xflow compare gstack --json`, `xflow assess --json`, `xflow launch audit --json`, `xflow launch claims --json`, `xflow launch copy --json` | Strong | +| One-shot evaluation | External evaluators can see quality score, Codex goal / Superpowers / Super-Assistant positioning, plus OpenSpec / gstack comparisons, launch blockers, release-owner status, claim boundaries, next actions, and first-run path from one command family. | `xflow evaluate`, `xflow evaluate --json`, `xflow release status`, `xflow release status --json`, `xflow compare codex-goal --json`, `xflow compare superpowers --json`, `xflow compare super-assistant --json`, `xflow compare openspec --json`, `xflow compare gstack --json`, `xflow assess --json`, `xflow launch audit --json`, `xflow launch claims --json`, `xflow launch copy --json` | Strong | | Spec durability | Plans, proposal artifacts, workflow deltas, AHA, and handoff survive thread changes. | `specs/changes/*`, `specs/workflow.md`, `AHA.md`, `HANDOFF.md` | Strong | | Cross-tool portability | Skills and workflows can be installed without personal machine assumptions. | `docs/compatibility.md`, `.as-xflow/config.json`, skillhub sync wrapper, installed skill diff | Strong | | Public product clarity | New users can understand the system without reading implementation internals. | README badges, `xflow quickstart`, `xflow guide`, `xflow evaluate`, `xflow assess`, `xflow demo launch`, `xflow launch dossier`, `xflow launch claims`, `xflow launch copy`, `xflow launch audit`, `xflow adoption status`, `xflow adoption trial`, `xflow package status`, `xflow package preflight`, `xflow package audit`, `xflow score`, `docs/quality-assessment.md`, `docs/launch-demo.md`, `docs/examples-gallery.md`, `docs/adoption/README.md`, `docs/methodology.md`, `docs/public-benchmark.md`, `RELEASE_NOTES.md`, `docs/fixtures/tracker-item.json`, this benchmark | Strong | @@ -42,6 +43,12 @@ Superpowers remains stronger as lightweight behavior discipline; xflow's stronger claim is repo-local workflow evidence, cross-tool state, and launch gates. +Against Super-Assistant, xflow should not claim broader concrete enterprise +connector inventory. Super-Assistant remains a useful reference for DingTalk, +plugin, checkpoint, experience, and S3 patterns. xflow's stronger claim is +absorbing those patterns as a policy overlay and thin atoms, keeping workflow +authority in local YAML, gates, and proof artifacts. + Against gstack, xflow should not claim better role choreography or browser rituals. The stronger xflow claim is repo-owned workflow truth: checked-in YAML, goal alignment, handoff/AHA continuity, and launch gates that can be audited @@ -86,6 +93,7 @@ xflow corps --explain --json xflow compare codex-goal xflow compare gstack xflow compare superpowers +xflow compare super-assistant xflow compare openspec xflow compare spec-kit xflow adapter import-file --input docs/fixtures/tracker-item.json --json diff --git a/docs/demo-proof.md b/docs/demo-proof.md index 246b3fb..61fb893 100644 --- a/docs/demo-proof.md +++ b/docs/demo-proof.md @@ -74,6 +74,7 @@ xflow score xflow goal audit --json xflow corps --explain --json xflow compare superpowers +xflow compare super-assistant xflow compare openspec xflow compare gstack xflow compare spec-kit diff --git a/docs/install-upgrade.md b/docs/install-upgrade.md index ce02944..cb973a4 100644 --- a/docs/install-upgrade.md +++ b/docs/install-upgrade.md @@ -93,6 +93,7 @@ xflow package preflight --check-registry --check-auth --json xflow launch audit --pre-publish --strict --json xflow package audit --check-registry --json xflow compare superpowers +xflow compare super-assistant xflow compare openspec xflow compare gstack xflow compare spec-kit diff --git a/docs/integrations.md b/docs/integrations.md index 68e1e59..eeeb2a9 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -15,6 +15,7 @@ openflow keeps integrations thin: external systems may supply issues, docs, CI, | CI Guard / Release Gate | `npm run release:pack`, `npm run publish:check`, `xflow adoption validate --json`, `xflow package preflight --check-registry --check-auth --json`, and `xflow package audit --check-registry --json` | Block public claims until local proof, package proof, registry proof, and real adoption evidence are green. | Implemented | | gstack | `xflow compare gstack` and `docs/gstack-comparison.md` | Keep the comparison fair: gstack wins role-specialist host commands and browser QA; xflow wins repo-owned workflow truth, proof, and launch gates. | Implemented | | Superpowers | `xflow compare superpowers` and `docs/superpowers-comparison.md` | Keep the comparison fair: Superpowers wins lightweight behavior guidance; xflow wins repo-local workflow evidence and launch gates. | Implemented | +| Super-Assistant | `xflow compare super-assistant` and `docs/super-assistant-comparison.md` | Keep the comparison fair: Super-Assistant wins raw enterprise connector breadth; xflow wins policy-overlay runtime, atomized provider seams, and structured proof. | Implemented | | OpenSpec | `xflow spec openspec-map` | Map OpenSpec projects into xflow without moving files. | Implemented | | spec-kit | `xflow compare spec-kit` and `docs/spec-kit-benchmark.md` | Keep a repeatable benchmark path against spec-kit-style delivery. | Implemented | diff --git a/docs/public-benchmark.md b/docs/public-benchmark.md index 1899d71..30b6010 100644 --- a/docs/public-benchmark.md +++ b/docs/public-benchmark.md @@ -27,6 +27,7 @@ xflow spec delta --change-id release-proof-adapter xflow compare codex-goal xflow compare gstack xflow compare superpowers +xflow compare super-assistant xflow compare openspec xflow compare spec-kit xflow score @@ -53,6 +54,7 @@ xflow launch audit --pre-publish --strict --json | TDD quality pressure | `I6a.tdd.run`, `I6b.tdd.proof_validate`, `I6c.tdd.quality_review`, and `test/tdd-proof.test.js`. | | gstack comparison | `docs/gstack-comparison.md` and `xflow compare gstack --json`. | | Superpowers comparison | `docs/superpowers-comparison.md` and `xflow compare superpowers --json`. | +| Super-Assistant comparison | `docs/super-assistant-comparison.md` and `xflow compare super-assistant --json`. | | OpenSpec migration | `xflow spec openspec-map`, `docs/openspec-migration.md`, and `xflow spec delta`. | | Spec authoring flow | `xflow spec start`, `docs/openspec-migration.md`, and generated `specs/changes/<id>/` artifacts. | | spec-kit comparison | `docs/spec-kit-benchmark.md`, `docs/integrations.md`, and `xflow compare spec-kit --json`. | diff --git a/docs/public-release.md b/docs/public-release.md index 29da0a5..6c7bb25 100644 --- a/docs/public-release.md +++ b/docs/public-release.md @@ -111,6 +111,7 @@ Record these in the release note or PR: - `docs/npm-publish-handoff.md` reviewed before real publish - `xflow score` - `xflow compare superpowers` +- `xflow compare super-assistant` - `xflow compare openspec` - `xflow compare gstack` - `xflow compare spec-kit` @@ -140,4 +141,5 @@ Record these in the release note or PR: - If `xflow compare spec-kit` points to missing docs, do not publish a superiority claim. - If `xflow compare gstack` cannot clearly explain where gstack is stronger, do not publish a fair comparison claim. - If `xflow compare superpowers` cannot explain where Superpowers is stronger, do not publish a fair comparison claim. +- If `xflow compare super-assistant` cannot explain where Super-Assistant keeps stronger enterprise connector breadth, do not publish a fair comparison claim. - If the team adoption operating model is not linked from README or reviewer docs, do not promote the package for multi-person adoption. diff --git a/docs/quality-assessment.md b/docs/quality-assessment.md index 0857ffc..34b6aa8 100644 --- a/docs/quality-assessment.md +++ b/docs/quality-assessment.md @@ -64,6 +64,7 @@ xflow evaluate xflow evaluate --json xflow compare codex-goal --json xflow compare superpowers --json +xflow compare super-assistant --json xflow compare gstack --json xflow demo launch xflow demo launch --json diff --git a/docs/super-assistant-comparison.md b/docs/super-assistant-comparison.md new file mode 100644 index 0000000..d005b6e --- /dev/null +++ b/docs/super-assistant-comparison.md @@ -0,0 +1,106 @@ +# Super-Assistant Comparison + +Use this page when someone asks whether openflow should absorb Super-Assistant's +enterprise delivery harness or split separate personal/team workflows. + +## Verdict + +Super-Assistant has useful enterprise ingredients: team constraints, checkpoint +notifications, plugin execution, an experience index, and S3-backed team sync. +Those ideas are worth absorbing. + +openflow should not absorb the whole harness. After the policy-overlay +integration, xflow is stronger because the same capabilities are expressed as +thin atoms, provider seams, and structured proof artifacts on top of the +existing `yolo` and `corps` tracks. + +```text +Super-Assistant = enterprise harness and connector inventory +xflow = workflow runtime plus policy overlay and atomized proof +``` + +## Scorecard + +Overall, xflow wins the productized workflow comparison: `95/100` versus +`76/100`. Super-Assistant still has broader concrete enterprise connector +coverage, but xflow now has the cleaner runtime contract. + +| Dimension | Weight | Super-Assistant | xflow | Winner | +| --- | ---: | ---: | ---: | --- | +| Workflow runtime authority | 15 | 10 | 15 | xflow | +| Policy overlay and team constraints | 15 | 12 | 15 | xflow | +| Adaptive intake | 10 | 8 | 10 | xflow | +| Enterprise atomization | 15 | 11 | 14 | xflow | +| Notification / gate separation | 10 | 6 | 10 | xflow | +| Experience reuse | 10 | 8 | 9 | xflow | +| Verification and packaging | 15 | 9 | 14 | xflow | +| Portability and secret boundary | 10 | 5 | 8 | xflow | + +Machine-readable form: + +```bash +xflow compare super-assistant --json +``` + +## What xflow Absorbed + +- `team_constraints.py` becomes `.xflow/profile.json`, + `.xflow/team-policy.json`, and `policy_effective.json`. Numeric thresholds + take the stricter value, lists merge and dedupe, and team strings/booleans + override personal defaults. +- Demand profiling becomes `workflow_recommendation.json`, generated by + `xflow intake recommend`, and remains advisory input rather than a workflow + bypass. +- Checkpoint notification becomes `notify.checkpoint`, which writes + `notification/checkpoint-*.json` and explicitly records + `approves_gate=false`. +- Plugin, storage, and experience reuse become `plugin.run`, `storage.sync`, + and `experience.digest`. The first provider set is `file`/`mock`/dry-run + safe, so DingTalk/S3-style secrets stay outside the core. +- Team enforcement becomes `policy.gate`, which emits structured + `failed_checks` for coverage, reviewer, smoke, and review-artifact + requirements. + +## Why Not Fork Team Workflows + +Do not create `team-yolo` or `personal-yolo`. That would duplicate workflow +YAML and drift as soon as a phase changes. + +The better split is execution mode: + +- `personal`: default, local-first, no external providers required. +- `team`: loads the team policy, enables optional notification/experience + atoms, and checks stricter gates. + +The workflow authority stays with `workflows/yolo.yaml`, +`workflows/corps.yaml`, the atom registry, and proof artifacts. External +systems may inform the run, but they do not advance state by themselves. + +## Proof Commands + +```bash +xflow compare super-assistant --json +xflow profile show --json +xflow policy validate --json +xflow intake recommend --change-id team-api-feature --title "team API feature" --description "touches API DB user data and CI" --repo-count 2 --json +xflow workflow validate yolo +xflow workflow validate corps +npm run verify +npm run drift:scan +``` + +For the reference checkout, run Super-Assistant's own validation commands and +compare the resulting evidence: + +```bash +npm run check +python3 -m unittest discover -s tests -p 'test*.py' -v +python3 -m unittest discover -s .harness -p 'test*.py' -v +``` + +## Boundary + +xflow should keep Super-Assistant's domain lessons, not its coupling model. +Concrete enterprise providers can be added later, but the first-class contract +must remain: notification is not approval, plugins are not workflow state, and +experience is advisory context rather than a gate substitute. diff --git a/docs/tooling-matrix.md b/docs/tooling-matrix.md index f859008..eec7fd2 100644 --- a/docs/tooling-matrix.md +++ b/docs/tooling-matrix.md @@ -17,6 +17,7 @@ xflow demo launch xflow launch dossier xflow compare codex-goal xflow compare superpowers +xflow compare super-assistant xflow compare gstack xflow init --project-root . xflow goal set "Ship the next verified change" --project-root . diff --git a/docs/walkthrough.md b/docs/walkthrough.md index a35ae95..df4076c 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -45,6 +45,7 @@ specs/changes/<change-id>/spec_delta_review.md ```bash xflow compare superpowers +xflow compare super-assistant xflow compare codex-goal xflow compare openspec xflow compare gstack diff --git a/docs/workflow-manual.md b/docs/workflow-manual.md index cb9184a..3a7f0cc 100644 --- a/docs/workflow-manual.md +++ b/docs/workflow-manual.md @@ -49,7 +49,7 @@ └────────────────────────────┬────────────────────────────────────┘ │ atom dispatch ┌────────────────────────────▼────────────────────────────────────┐ -│ L0 原子组件(76 atoms,每个只有一处实现) │ +│ L0 原子组件(83 atoms,每个只有一处实现) │ │ │ │ xflow/atoms/*.py A/B/C/E6/H1/I6/J/K (lite) │ │ src/core/atoms/**/*.js D/E/F/G/E3/E4/E7 (heavy) │ diff --git a/src/core/atoms/experience/digest.js b/src/core/atoms/experience/digest.js new file mode 100644 index 0000000..07ca79f --- /dev/null +++ b/src/core/atoms/experience/digest.js @@ -0,0 +1,55 @@ +import { existsSync, readdirSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { readJsonFile, resolveChangePath, writeJsonFile } from '../../change-artifacts.js'; + +export async function experienceDigest(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id; + if (!changeId) throw new Error('change_id is required for experience.digest'); + + const entries = collectExperienceEntries(projectRoot); + const digest = { + version: 1, + change_id: changeId, + generated_at: new Date().toISOString(), + advisory_only: true, + entry_count: entries.length, + entries: entries.map((entry) => ({ + title: entry.title || entry.summary || 'Untitled experience', + category: entry.category || 'workflow', + content: entry.content || entry.summary || '', + confidence: entry.confidence ?? null, + source: entry.source || null, + })), + }; + const outputPath = resolveChangePath(projectRoot, changeId, 'experience_digest.json'); + writeJsonFile(outputPath, digest); + return { + ok: true, + experience_digest_file: outputPath, + digest, + }; +} + +function collectExperienceEntries(projectRoot) { + const sources = []; + const localDir = resolve(projectRoot, '.xflow', 'experience'); + if (existsSync(localDir)) { + for (const file of readdirSync(localDir)) { + if (file.endsWith('.json')) sources.push(resolve(localDir, file)); + } + } + const teamExperience = resolve(projectRoot, '.xflow', 'storage', 'team', 'experience.json'); + if (existsSync(teamExperience)) sources.push(teamExperience); + + const entries = []; + for (const source of sources) { + const payload = readJsonFile(source, {}); + const sourceEntries = Array.isArray(payload.entries) ? payload.entries : Array.isArray(payload) ? payload : []; + for (const entry of sourceEntries) { + entries.push({ ...entry, source }); + } + } + return entries; +} diff --git a/src/core/atoms/intake/recommend.js b/src/core/atoms/intake/recommend.js new file mode 100644 index 0000000..c858406 --- /dev/null +++ b/src/core/atoms/intake/recommend.js @@ -0,0 +1,20 @@ +import { resolveEffectivePolicy } from '../../policy-overlay.js'; +import { writeWorkflowRecommendation } from '../../intake-recommendation.js'; + +export async function intakeRecommend(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id; + if (!changeId) throw new Error('change_id is required for G5.intake.recommend'); + const policyResult = resolveEffectivePolicy({ + projectRoot, + changeId, + executionMode: input.execution_mode || null, + writeArtifact: true, + }); + return writeWorkflowRecommendation({ + projectRoot, + changeId, + input: { ...(context.input || {}), ...input }, + effectivePolicy: policyResult.policy || policyResult.profile || {}, + }); +} diff --git a/src/core/atoms/notify/checkpoint.js b/src/core/atoms/notify/checkpoint.js new file mode 100644 index 0000000..b5202bb --- /dev/null +++ b/src/core/atoms/notify/checkpoint.js @@ -0,0 +1,46 @@ +import { resolveChangePath, writeJsonFile } from '../../change-artifacts.js'; + +export async function notifyCheckpoint(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id; + if (!changeId) throw new Error('change_id is required for notify.checkpoint'); + + const phase = input.phase || context.phase?.id || 'checkpoint'; + const provider = input.provider || 'file'; + if (!['file', 'mock'].includes(provider)) { + return { + ok: false, + error: `Unsupported notify provider: ${provider}`, + supported_providers: ['file', 'mock'], + }; + } + + const evidence = { + version: 1, + provider, + phase, + change_id: changeId, + mission_id: input.mission_id || context.runtime?.missionId || null, + recipient: input.recipient || null, + artifact_url: input.artifact_url || input.artifact || null, + message: input.message || `Checkpoint notification for ${phase}`, + sent_at: new Date().toISOString(), + status: provider === 'mock' ? 'mock_sent' : 'sent', + approves_gate: false, + authority: 'notification_only_human_gate_ack_required', + }; + const outputPath = resolveChangePath(projectRoot, changeId, `notification/checkpoint-${sanitizeFilePart(phase)}.json`); + writeJsonFile(outputPath, evidence); + return { + ok: true, + provider, + phase, + notification_file: outputPath, + approves_gate: false, + evidence, + }; +} + +function sanitizeFilePart(value) { + return String(value || 'checkpoint').replace(/[^a-zA-Z0-9._-]+/g, '-'); +} diff --git a/src/core/atoms/plugin/run.js b/src/core/atoms/plugin/run.js new file mode 100644 index 0000000..b280e90 --- /dev/null +++ b/src/core/atoms/plugin/run.js @@ -0,0 +1,76 @@ +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +import { resolveChangePath, writeJsonFile } from '../../change-artifacts.js'; + +export async function pluginRun(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id; + if (!changeId) throw new Error('change_id is required for plugin.run'); + + const type = input.type || 'script'; + const ref = input.ref || ''; + const dryRun = input.dry_run !== false; + const payload = input.payload || {}; + const result = dryRun + ? dryRunResult(type, ref, payload) + : executePlugin({ type, ref, payload, projectRoot, timeoutMs: Number(input.timeout_ms || 60000) }); + + const output = { + version: 1, + change_id: changeId, + type, + ref, + dry_run: dryRun, + generated_at: new Date().toISOString(), + result, + }; + const outputPath = resolveChangePath(projectRoot, changeId, `plugin/${sanitizeFilePart(type)}-${sanitizeFilePart(ref || 'plugin')}.json`); + writeJsonFile(outputPath, output); + return { + ok: result.status !== 'error', + plugin_result_file: outputPath, + result, + }; +} + +function dryRunResult(type, ref, payload) { + if (!['script', 'webhook', 'agent', 'skill', 'interactive'].includes(type)) { + return { status: 'error', error: `Unknown plugin type: ${type}` }; + } + return { + status: 'dry_run', + type, + ref, + payload_preview: payload, + message: 'Plugin execution was recorded without external side effects.', + }; +} + +function executePlugin({ type, ref, payload, projectRoot, timeoutMs }) { + if (type !== 'script') { + return { status: 'error', error: `Non-dry-run ${type} plugins are not supported by the generic v1 provider` }; + } + const scriptPath = resolve(projectRoot, ref); + const started = Date.now(); + const result = spawnSync('bash', [scriptPath], { + cwd: projectRoot, + encoding: 'utf8', + timeout: timeoutMs, + env: { + ...process.env, + PAYLOAD: JSON.stringify(payload), + }, + }); + return { + status: result.status === 0 ? 'ok' : 'error', + exit_code: result.status, + duration_ms: Date.now() - started, + stdout: result.stdout || '', + stderr: result.stderr || '', + }; +} + +function sanitizeFilePart(value) { + return String(value || 'plugin').replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 80); +} diff --git a/src/core/atoms/policy/gate.js b/src/core/atoms/policy/gate.js new file mode 100644 index 0000000..cdd089a --- /dev/null +++ b/src/core/atoms/policy/gate.js @@ -0,0 +1,90 @@ +import { readJsonFile, resolveChangePath, writeJsonFile } from '../../change-artifacts.js'; +import { resolveEffectivePolicy } from '../../policy-overlay.js'; + +export async function policyGate(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id; + if (!changeId) throw new Error('change_id is required for P2.policy.gate'); + + const policyResult = resolveEffectivePolicy({ + projectRoot, + changeId, + executionMode: input.execution_mode || null, + writeArtifact: true, + }); + if (!policyResult.ok) { + const packet = { + version: 1, + ok: false, + generated_at: new Date().toISOString(), + failed_checks: [{ code: policyResult.error.code, message: policyResult.error.message }], + policy_error: policyResult.error, + }; + const outputPath = resolveChangePath(projectRoot, changeId, 'policy_gate.json'); + writeJsonFile(outputPath, packet); + return { ...packet, policy_gate_file: outputPath }; + } + + const policy = policyResult.policy; + const failedChecks = [ + ...checkCoverage(projectRoot, changeId, policy), + ...checkReviewers(projectRoot, changeId, policy), + ...checkSmoke(projectRoot, changeId, policy), + ]; + const packet = { + version: 1, + ok: failedChecks.length === 0, + generated_at: new Date().toISOString(), + execution_mode: policy.execution_mode || 'personal', + failed_checks: failedChecks, + advisory: policy.execution_mode !== 'team', + }; + const outputPath = resolveChangePath(projectRoot, changeId, 'policy_gate.json'); + writeJsonFile(outputPath, packet); + return { ...packet, policy_gate_file: outputPath }; +} + +function checkCoverage(projectRoot, changeId, policy) { + const threshold = policy?.coding?.min_changed_line_coverage; + if (threshold === undefined) return []; + const quality = readJsonFile(resolveChangePath(projectRoot, changeId, 'tdd/quality-0.json'), {}); + const actual = Number( + quality.changed_line_coverage + ?? quality.incremental_line_coverage + ?? quality.coverage + ?? quality.gate_value, + ); + if (!Number.isFinite(actual)) { + return [{ code: 'coverage_evidence_missing', message: 'changed-line coverage evidence is missing', expected: Number(threshold), actual: null }]; + } + return actual >= Number(threshold) + ? [] + : [{ code: 'coverage_below_policy', message: 'changed-line coverage is below team policy', expected: Number(threshold), actual }]; +} + +function checkReviewers(projectRoot, changeId, policy) { + const threshold = policy?.review?.min_reviewers; + if (threshold === undefined) return []; + const review = readJsonFile(resolveChangePath(projectRoot, changeId, 'review.json'), {}); + const reviewers = Array.isArray(review.reviewers) + ? review.reviewers + : Array.isArray(review.approved_by) + ? review.approved_by + : []; + return reviewers.length >= Number(threshold) + ? [] + : [{ code: 'reviewers_below_policy', message: 'reviewer count is below team policy', expected: Number(threshold), actual: reviewers.length }]; +} + +function checkSmoke(projectRoot, changeId, policy) { + const threshold = policy?.deploy?.required_smoke_pass_rate; + if (threshold === undefined) return []; + const smoke = readJsonFile(resolveChangePath(projectRoot, changeId, 'smoke.json'), {}); + const actual = Number(smoke.pass_rate ?? smoke.required_smoke_pass_rate ?? smoke.smoke_pass_rate); + if (!Number.isFinite(actual)) { + return [{ code: 'smoke_evidence_missing', message: 'smoke pass-rate evidence is missing', expected: Number(threshold), actual: null }]; + } + return actual >= Number(threshold) + ? [] + : [{ code: 'smoke_pass_rate_below_policy', message: 'smoke pass rate is below team policy', expected: Number(threshold), actual }]; +} diff --git a/src/core/atoms/policy/resolve.js b/src/core/atoms/policy/resolve.js new file mode 100644 index 0000000..97a8b77 --- /dev/null +++ b/src/core/atoms/policy/resolve.js @@ -0,0 +1,11 @@ +import { resolveEffectivePolicy } from '../../policy-overlay.js'; + +export async function policyResolve(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id || null; + return resolveEffectivePolicy({ + projectRoot, + changeId, + executionMode: input.execution_mode || null, + }); +} diff --git a/src/core/atoms/storage/sync.js b/src/core/atoms/storage/sync.js new file mode 100644 index 0000000..c472c9e --- /dev/null +++ b/src/core/atoms/storage/sync.js @@ -0,0 +1,57 @@ +import { copyFileSync, existsSync, mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; + +import { resolveChangePath, writeJsonFile } from '../../change-artifacts.js'; + +export async function storageSync(input = {}, context = {}) { + const projectRoot = context.projectRoot || process.cwd(); + const changeId = context.changeId || input.change_id; + if (!changeId) throw new Error('change_id is required for storage.sync'); + + const provider = input.provider || 'file'; + const direction = input.direction || 'import'; + if (!['file', 'mock'].includes(provider)) { + return { ok: false, error: `Unsupported storage provider: ${provider}` }; + } + if (!['import', 'export'].includes(direction)) { + return { ok: false, error: `Unsupported storage direction: ${direction}` }; + } + + const source = input.source ? resolve(projectRoot, input.source) : null; + const target = resolve(projectRoot, '.xflow', 'storage', normalizeStoragePath(input.target || 'artifact.json')); + let copied = false; + if (provider === 'file' && direction === 'import') { + if (!source || !existsSync(source)) { + return { ok: false, error: `storage source not found: ${source || '(missing)'}` }; + } + mkdirSync(dirname(target), { recursive: true }); + copyFileSync(source, target); + copied = true; + } + + const evidence = { + version: 1, + provider, + direction, + source, + target, + copied, + generated_at: new Date().toISOString(), + }; + const outputPath = resolveChangePath(projectRoot, changeId, `storage/${provider}-${direction}.json`); + writeJsonFile(outputPath, evidence); + return { + ok: true, + storage_sync_file: outputPath, + storage_target: target, + evidence, + }; +} + +function normalizeStoragePath(value) { + return String(value || 'artifact.json') + .replace(/^\.+/, '') + .replace(/\\/g, '/') + .replace(/\/+/g, '/') + .replace(/^\/+/, ''); +} diff --git a/src/core/intake-recommendation.js b/src/core/intake-recommendation.js new file mode 100644 index 0000000..2fc67ea --- /dev/null +++ b/src/core/intake-recommendation.js @@ -0,0 +1,134 @@ +import { resolveChangePath, writeJsonFile } from './change-artifacts.js'; + +const TYPE_PATTERNS = [ + ['hotfix', ['hotfix', '紧急修复', '线上问题', 'p0']], + ['bugfix', ['bug', 'fix', '修复', '缺陷', '报错']], + ['config-change', ['config', '配置', '开关', '阈值', '环境变量']], + ['docs', ['doc', 'docs', 'readme', '文档', '说明']], + ['refactor', ['refactor', '重构', '技术债', '解耦']], + ['feature', ['feature', '新增', '功能', '需求']], +]; + +export function buildWorkflowRecommendation(input = {}, effectivePolicy = {}) { + const profile = buildDemandProfile(input); + const mode = effectivePolicy.execution_mode || input.execution_mode || 'personal'; + const recommendedTrack = chooseTrack(profile); + const requiresCoverageGate = shouldRequireCoverage(profile, effectivePolicy); + const requiresHumanCheckpoint = ['critical', 'elevated'].includes(profile.risk_level) + || profile.complexity === 'critical'; + const requiresTeamNotification = mode === 'team' && (requiresHumanCheckpoint || recommendedTrack === 'corps'); + + return { + version: 1, + generated_at: new Date().toISOString(), + recommended_track: recommendedTrack, + requires_coverage_gate: requiresCoverageGate, + requires_human_checkpoint: requiresHumanCheckpoint, + requires_team_notification: requiresTeamNotification, + profile, + policy_overlay: { + execution_mode: mode, + workflows_remain: ['yolo', 'corps'], + creates_team_workflow_fork: false, + }, + reasons: buildReasons(profile, recommendedTrack, { + requiresCoverageGate, + requiresHumanCheckpoint, + requiresTeamNotification, + }), + }; +} + +export function writeWorkflowRecommendation({ projectRoot, changeId, input = {}, effectivePolicy = {} }) { + const recommendation = buildWorkflowRecommendation(input, effectivePolicy); + const outputPath = resolveChangePath(projectRoot, changeId, 'workflow_recommendation.json'); + writeJsonFile(outputPath, recommendation); + return { ok: true, recommendation, workflow_recommendation_file: outputPath }; +} + +export function buildDemandProfile(input = {}) { + const title = String(input.title || input.requirement_title || input.req_name || '').trim(); + const description = String(input.description || input.requirement_description || '').trim(); + const combined = `${title} ${description}`.toLowerCase(); + const repoCount = normalizeRepoCount(input); + const estimatedFileCount = Number(input.estimated_file_count || input.file_count || 1); + const demandType = input.demand_type || input.change_type || classifyDemandType(combined); + const affectsApi = hasAny(combined, ['api', '接口', 'endpoint', 'rpc', 'grpc']); + const affectsDatabase = hasAny(combined, ['database', 'db', '数据库', 'ddl', 'migration', 'schema']); + const involvesPayment = hasAny(combined, ['payment', '支付', 'pay', 'billing', '结算']); + const involvesUserData = hasAny(combined, ['user data', '用户数据', '隐私', '个人信息']); + const riskLevel = assessRisk({ involvesPayment, involvesUserData, affectsDatabase, repoCount }); + const complexity = assessComplexity({ repoCount, estimatedFileCount, affectsApi, affectsDatabase, riskLevel }); + + return { + demand_type: normalizeDemandType(demandType), + complexity, + risk_level: riskLevel, + repo_count: repoCount, + estimated_file_count: estimatedFileCount, + cross_service: repoCount > 1, + affects_api: affectsApi, + affects_database: affectsDatabase, + involves_payment: involvesPayment, + involves_user_data: involvesUserData, + title, + }; +} + +function classifyDemandType(text) { + for (const [type, patterns] of TYPE_PATTERNS) { + if (patterns.some((pattern) => text.includes(pattern))) return type; + } + return 'feature'; +} + +function normalizeDemandType(type) { + if (type === 'documentation') return 'docs'; + if (type === 'backend' || type === 'frontend' || type === 'full-stack') return 'feature'; + return type || 'feature'; +} + +function normalizeRepoCount(input = {}) { + if (Array.isArray(input.repositories)) return input.repositories.length || 1; + const value = Number(input.repo_count || input.repositories || 1); + return Number.isFinite(value) && value > 0 ? value : 1; +} + +function assessRisk({ involvesPayment, involvesUserData, affectsDatabase, repoCount }) { + if (involvesPayment || involvesUserData) return 'critical'; + if (affectsDatabase || repoCount > 1) return 'elevated'; + return 'standard'; +} + +function assessComplexity({ repoCount, estimatedFileCount, affectsApi, affectsDatabase, riskLevel }) { + if (riskLevel === 'critical' || repoCount > 3 || estimatedFileCount > 20) return 'critical'; + if (repoCount > 1 || estimatedFileCount > 8 || affectsDatabase) return 'high'; + if (estimatedFileCount > 3 || affectsApi) return 'medium'; + return 'low'; +} + +function chooseTrack(profile) { + if (profile.demand_type === 'docs' && profile.risk_level === 'standard') return 'yolo'; + if (profile.risk_level === 'critical' || profile.complexity === 'critical' || profile.cross_service) return 'corps'; + return 'yolo'; +} + +function shouldRequireCoverage(profile, policy = {}) { + if (profile.demand_type === 'docs') return false; + if (policy?.coding?.min_changed_line_coverage !== undefined) return true; + return ['feature', 'bugfix', 'hotfix', 'refactor'].includes(profile.demand_type); +} + +function buildReasons(profile, track, flags) { + const reasons = [`track:${track}`]; + if (profile.cross_service) reasons.push('cross_service'); + if (profile.risk_level !== 'standard') reasons.push(`risk:${profile.risk_level}`); + if (flags.requiresCoverageGate) reasons.push('coverage_gate'); + if (flags.requiresHumanCheckpoint) reasons.push('human_checkpoint'); + if (flags.requiresTeamNotification) reasons.push('team_notification'); + return reasons; +} + +function hasAny(text, patterns) { + return patterns.some((pattern) => text.includes(pattern)); +} diff --git a/src/core/policy-overlay.js b/src/core/policy-overlay.js new file mode 100644 index 0000000..9deec16 --- /dev/null +++ b/src/core/policy-overlay.js @@ -0,0 +1,180 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; + +import { resolveChangePath, writeJsonFile } from './change-artifacts.js'; + +const DEFAULT_PROFILE = { + version: 1, + execution_mode: 'personal', + workflow_tracks: { + personal: 'yolo|corps', + team: 'yolo|corps', + }, +}; + +function readJson(path, fallback = null) { + if (!existsSync(path)) return fallback; + try { + const raw = readFileSync(path, 'utf8'); + return raw.trim() ? JSON.parse(raw) : fallback; + } catch { + return fallback; + } +} + +function writeJson(path, payload) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + return path; +} + +export function resolveProfilePath(projectRoot) { + return resolve(projectRoot, '.xflow', 'profile.json'); +} + +export function resolveTeamPolicyPath(projectRoot) { + return resolve(projectRoot, '.xflow', 'team-policy.json'); +} + +export function readProfile(projectRoot) { + const profile = readJson(resolveProfilePath(projectRoot), {}); + return mergePolicy(DEFAULT_PROFILE, profile || {}); +} + +export function writeProfile(projectRoot, patch = {}) { + const current = readProfile(projectRoot); + const next = mergePolicy(current, patch); + writeJson(resolveProfilePath(projectRoot), next); + return next; +} + +export function readTeamPolicy(projectRoot) { + return readJson(resolveTeamPolicyPath(projectRoot), null); +} + +export function validateTeamPolicy(policy = {}) { + const errors = []; + const coverage = policy?.coding?.min_changed_line_coverage; + if (coverage !== undefined && (!Number.isFinite(Number(coverage)) || Number(coverage) < 0 || Number(coverage) > 100)) { + errors.push('coding.min_changed_line_coverage must be a number between 0 and 100'); + } + const reviewers = policy?.review?.min_reviewers; + if (reviewers !== undefined && (!Number.isFinite(Number(reviewers)) || Number(reviewers) < 0)) { + errors.push('review.min_reviewers must be a non-negative number'); + } + const smoke = policy?.deploy?.required_smoke_pass_rate; + if (smoke !== undefined && (!Number.isFinite(Number(smoke)) || Number(smoke) < 0 || Number(smoke) > 100)) { + errors.push('deploy.required_smoke_pass_rate must be a number between 0 and 100'); + } + return { ok: errors.length === 0, errors }; +} + +export function mergePolicy(personal = {}, team = {}) { + const merged = mergeValue(personal || {}, team || {}); + merged.workflow_tracks = { + personal: 'yolo|corps', + team: 'yolo|corps', + ...(merged.workflow_tracks || {}), + }; + return merged; +} + +function mergeValue(personal, team) { + if (team === undefined || team === null) return cloneValue(personal); + if (personal === undefined || personal === null) return cloneValue(team); + + if (Array.isArray(personal) || Array.isArray(team)) { + return mergeLists(team, personal); + } + if (typeof personal === 'number' || typeof team === 'number') { + const personalNumber = Number(personal); + const teamNumber = Number(team); + if (Number.isFinite(personalNumber) && Number.isFinite(teamNumber)) { + return Math.max(personalNumber, teamNumber); + } + return cloneValue(team); + } + if (isPlainObject(personal) && isPlainObject(team)) { + const result = {}; + for (const key of new Set([...Object.keys(personal), ...Object.keys(team)])) { + result[key] = mergeValue(personal[key], team[key]); + } + return result; + } + return cloneValue(team); +} + +function mergeLists(teamList, personalList) { + const values = [ + ...(Array.isArray(teamList) ? teamList : [teamList]), + ...(Array.isArray(personalList) ? personalList : [personalList]), + ].filter((value) => value !== undefined && value !== null); + const seen = new Set(); + const merged = []; + for (const value of values) { + const key = JSON.stringify(value); + if (seen.has(key)) continue; + seen.add(key); + merged.push(cloneValue(value)); + } + return merged; +} + +function isPlainObject(value) { + return value && typeof value === 'object' && !Array.isArray(value); +} + +function cloneValue(value) { + if (value === undefined) return undefined; + return JSON.parse(JSON.stringify(value)); +} + +export function resolveEffectivePolicy({ projectRoot, changeId = null, executionMode = null, writeArtifact = true } = {}) { + const profile = readProfile(projectRoot); + if (executionMode) { + profile.execution_mode = executionMode; + } + + const mode = profile.execution_mode || 'personal'; + const teamPolicy = readTeamPolicy(projectRoot); + if (mode === 'team' && !teamPolicy) { + return { + ok: false, + error: { + code: 'team_policy_missing', + message: 'execution_mode=team requires .xflow/team-policy.json', + }, + profile, + policy: null, + }; + } + + const validation = teamPolicy ? validateTeamPolicy(teamPolicy) : { ok: true, errors: [] }; + if (!validation.ok) { + return { + ok: false, + error: { + code: 'team_policy_invalid', + message: 'team policy failed validation', + errors: validation.errors, + }, + profile, + policy: null, + }; + } + + const policy = mode === 'team' ? mergePolicy(profile, teamPolicy) : mergePolicy(DEFAULT_PROFILE, profile); + policy.execution_mode = mode; + policy.generated_at = new Date().toISOString(); + policy.source = { + profile: resolveProfilePath(projectRoot), + team_policy: teamPolicy ? resolveTeamPolicyPath(projectRoot) : null, + }; + policy.authority = 'policy_overlay_only_workflows_remain_yolo_corps'; + + const result = { ok: true, profile, team_policy: teamPolicy, policy }; + if (writeArtifact && changeId) { + result.policy_effective_file = writeJsonFile(resolveChangePath(projectRoot, changeId, 'policy_effective.json'), policy); + } + return result; +} diff --git a/test/cli-usability.test.js b/test/cli-usability.test.js index b075ab1..ac8b013 100644 --- a/test/cli-usability.test.js +++ b/test/cli-usability.test.js @@ -2413,6 +2413,8 @@ phases: assert.ok(payload.competitive.codex_goal.boundary.not_for.some((item) => item.includes('replacing plan.md'))); assert.equal(payload.competitive.superpowers.target, 'superpowers'); assert.ok(payload.competitive.superpowers.target_edges.some((edge) => edge.includes('behavioral guidance'))); + assert.equal(payload.competitive.super_assistant.target, 'super-assistant'); + assert.ok(payload.competitive.super_assistant.target_edges.some((edge) => edge.includes('enterprise harness'))); assert.ok(payload.quickstart.source_checkout.commands.includes('npm run release:pack')); assert.ok(payload.quickstart.release_owner_gates.commands.includes('xflow release status --json')); assert.ok(payload.quickstart.release_owner_gates.commands.includes('xflow adoption trial --name <team-or-project> --source <tracker-or-pr> --track yolo')); @@ -2425,6 +2427,7 @@ phases: assert.ok(payload.proof_commands.includes('xflow release status --json')); assert.ok(payload.proof_commands.includes('xflow compare codex-goal --json')); assert.ok(payload.proof_commands.includes('xflow compare superpowers --json')); + assert.ok(payload.proof_commands.includes('xflow compare super-assistant --json')); assert.ok(payload.proof_commands.includes('xflow adoption status --json')); assert.ok(payload.proof_commands.includes('xflow package status --json')); assert.ok(payload.proof_commands.includes('xflow adoption trial --name <team-or-project> --source <tracker-or-pr> --track yolo --json')); @@ -4318,6 +4321,24 @@ Repository names were sanitized. assert.ok(superpowersPayload.target_edges.some((edge) => edge.includes('behavioral guidance'))); assert.ok(superpowersPayload.xflow_edges.some((edge) => edge.includes('xflow coach'))); + result = spawnSync('node', ['bin/xflow.js', 'compare', 'super-assistant', '--json'], { + cwd: REPO_ROOT, + encoding: 'utf8', + timeout: 3000, + }); + + assert.equal(result.status, 0, result.stderr || result.stdout || String(result.error)); + const superAssistantPayload = JSON.parse(result.stdout); + assert.equal(superAssistantPayload.ok, true); + assert.equal(superAssistantPayload.target, 'super-assistant'); + assert.equal(superAssistantPayload.winner.overall, 'xflow'); + assert.equal(superAssistantPayload.scorecard.overall.xflow, 95); + assert.equal(superAssistantPayload.scorecard.overall['super-assistant'], 76); + assert.ok(superAssistantPayload.scorecard.dimensions.some((dimension) => dimension.id === 'policy_overlay_and_team_constraints' && dimension.winner === 'xflow')); + assert.ok(superAssistantPayload.scorecard.dimensions.some((dimension) => dimension.id === 'notification_gate_separation' && dimension.evidence_refs.some((ref) => ref.includes('approves_gate=false')))); + assert.ok(superAssistantPayload.xflow_edges.some((edge) => edge.includes('policy_effective.json'))); + assert.ok(superAssistantPayload.target_edges.some((edge) => edge.includes('team_constraints.py'))); + result = spawnSync('node', ['bin/xflow.js', 'compare', 'codex-goal', '--json'], { cwd: REPO_ROOT, encoding: 'utf8', diff --git a/test/competitive-readiness.test.js b/test/competitive-readiness.test.js index 33b3c7f..ab1ab11 100644 --- a/test/competitive-readiness.test.js +++ b/test/competitive-readiness.test.js @@ -48,6 +48,7 @@ describe('competitive readiness benchmark', () => { 'docs/openspec-migration.md', 'docs/spec-kit-benchmark.md', 'docs/superpowers-comparison.md', + 'docs/super-assistant-comparison.md', 'docs/public-release.md', 'docs/public-benchmark.md', 'docs/npm-publish-handoff.md', @@ -94,7 +95,7 @@ describe('competitive readiness benchmark', () => { 'Cross-tool portability', 'Goal alignment', 'One-shot evaluation', - 'Codex goal / Superpowers positioning', + 'Codex goal / Superpowers / Super-Assistant positioning', 'xflow goal set', 'xflow goal show --json', 'xflow goal audit --json', @@ -132,6 +133,7 @@ describe('competitive readiness benchmark', () => { 'xflow compare openspec', 'xflow compare spec-kit', 'xflow compare superpowers', + 'xflow compare super-assistant', 'xflow adapter import-file', 'npm run release:pack', 'npm run publish:check', @@ -143,6 +145,8 @@ describe('competitive readiness benchmark', () => { assert.match(benchmark, /Superpowers/); assert.match(benchmark, /xflow compare superpowers/); + assert.match(benchmark, /Super-Assistant/); + assert.match(benchmark, /xflow compare super-assistant/); assert.match(benchmark, /OpenSpec/); assert.match(benchmark, /spec-kit/); assert.match(benchmark, /gstack/); @@ -210,6 +214,29 @@ describe('competitive readiness benchmark', () => { } }); + test('Super-Assistant comparison protects policy-overlay absorption boundary', () => { + const comparison = readFileSync(resolve(REPO_ROOT, 'docs', 'super-assistant-comparison.md'), 'utf8'); + + for (const phrase of [ + '95/100', + '76/100', + 'Policy overlay and team constraints', + 'Enterprise atomization', + 'Notification / gate separation', + 'xflow compare super-assistant --json', + 'xflow policy validate --json', + 'xflow intake recommend', + 'notify.checkpoint', + 'approves_gate=false', + 'plugin.run', + 'storage.sync', + 'experience.digest', + 'notification is not approval', + ]) { + assert.match(comparison, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } + }); + test('methodology guide exposes the natural delivery loop', () => { const methodology = readFileSync(resolve(REPO_ROOT, 'docs', 'methodology.md'), 'utf8'); const readme = readFileSync(resolve(REPO_ROOT, 'xflow', 'README.md'), 'utf8'); @@ -470,6 +497,7 @@ describe('competitive readiness benchmark', () => { 'xflow compare openspec', 'xflow compare spec-kit', 'xflow compare superpowers', + 'xflow compare super-assistant', ]) { assert.match(demoProof, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } @@ -536,6 +564,7 @@ describe('competitive readiness benchmark', () => { 'Confluence', 'CI Guard', 'Superpowers', + 'Super-Assistant', 'OpenSpec', 'spec-kit', ]) { @@ -558,6 +587,7 @@ describe('competitive readiness benchmark', () => { 'xflow adoption trial', 'xflow compare codex-goal', 'xflow compare superpowers', + 'xflow compare super-assistant', 'xflow compare openspec', 'xflow compare spec-kit', 'npm run publish:check', @@ -614,6 +644,7 @@ describe('competitive readiness benchmark', () => { 'xflow compare openspec', 'xflow compare spec-kit', 'xflow compare superpowers', + 'xflow compare super-assistant', 'xflow adapter import-file', 'npm run publish:check', 'spec_delta_review.json', diff --git a/test/policy-overlay.test.js b/test/policy-overlay.test.js new file mode 100644 index 0000000..7310f5f --- /dev/null +++ b/test/policy-overlay.test.js @@ -0,0 +1,329 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { spawnSync } from 'node:child_process'; + +import { + mergePolicy, + readProfile, + readTeamPolicy, + resolveEffectivePolicy, + validateTeamPolicy, +} from '../src/core/policy-overlay.js'; +import { buildWorkflowRecommendation } from '../src/core/intake-recommendation.js'; +import { notifyCheckpoint } from '../src/core/atoms/notify/checkpoint.js'; +import { policyGate } from '../src/core/atoms/policy/gate.js'; +import { pluginRun } from '../src/core/atoms/plugin/run.js'; +import { storageSync } from '../src/core/atoms/storage/sync.js'; +import { experienceDigest } from '../src/core/atoms/experience/digest.js'; + +const REPO_ROOT = process.cwd(); + +function makeProjectRoot() { + const root = mkdtempSync(join(tmpdir(), 'openflow-policy-')); + mkdirSync(resolve(root, '.xflow'), { recursive: true }); + mkdirSync(resolve(root, 'specs', 'changes', 'chg-policy-1'), { recursive: true }); + return root; +} + +function cleanupProjectRoot(root) { + rmSync(root, { recursive: true, force: true }); +} + +function readJson(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} + +describe('policy overlay', () => { + test('merges personal profile and team policy without forking workflow tracks', () => { + const personal = { + execution_mode: 'personal', + coding: { + min_changed_line_coverage: 60, + forbidden_patterns: ['large function'], + }, + review: { + min_reviewers: 1, + }, + }; + const team = { + execution_mode: 'team', + coding: { + min_changed_line_coverage: 80, + forbidden_patterns: ['snapshot-only tests', 'large function'], + require_meaningful_tests: true, + }, + review: { + min_reviewers: 2, + focus_categories: ['security'], + }, + }; + + const merged = mergePolicy(personal, team); + + assert.equal(merged.execution_mode, 'team'); + assert.equal(merged.coding.min_changed_line_coverage, 80); + assert.deepEqual(merged.coding.forbidden_patterns, ['snapshot-only tests', 'large function']); + assert.equal(merged.coding.require_meaningful_tests, true); + assert.equal(merged.review.min_reviewers, 2); + assert.deepEqual(merged.review.focus_categories, ['security']); + assert.equal(merged.workflow_tracks.personal, 'yolo|corps'); + assert.equal(merged.workflow_tracks.team, 'yolo|corps'); + }); + + test('reads personal defaults and writes effective policy artifact for team mode', () => { + const projectRoot = makeProjectRoot(); + try { + writeFileSync(resolve(projectRoot, '.xflow', 'profile.json'), JSON.stringify({ + execution_mode: 'team', + coding: { min_changed_line_coverage: 60 }, + review: { min_reviewers: 1 }, + }, null, 2)); + writeFileSync(resolve(projectRoot, '.xflow', 'team-policy.json'), JSON.stringify({ + coding: { min_changed_line_coverage: 85 }, + review: { min_reviewers: 2 }, + }, null, 2)); + + assert.equal(readProfile(projectRoot).execution_mode, 'team'); + assert.equal(readTeamPolicy(projectRoot).coding.min_changed_line_coverage, 85); + + const result = resolveEffectivePolicy({ projectRoot, changeId: 'chg-policy-1' }); + assert.equal(result.ok, true); + assert.equal(result.policy.execution_mode, 'team'); + assert.equal(result.policy.coding.min_changed_line_coverage, 85); + assert.ok(existsSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'policy_effective.json'))); + } finally { + cleanupProjectRoot(projectRoot); + } + }); + + test('team mode reports a clear policy error when team-policy is missing', () => { + const projectRoot = makeProjectRoot(); + try { + writeFileSync(resolve(projectRoot, '.xflow', 'profile.json'), JSON.stringify({ + execution_mode: 'team', + }, null, 2)); + + const result = resolveEffectivePolicy({ projectRoot, changeId: 'chg-policy-1' }); + + assert.equal(result.ok, false); + assert.equal(result.error.code, 'team_policy_missing'); + } finally { + cleanupProjectRoot(projectRoot); + } + }); + + test('rejects invalid team policy shapes before gates consume them', () => { + const validation = validateTeamPolicy({ + coding: { min_changed_line_coverage: 'high' }, + review: { min_reviewers: -1 }, + }); + + assert.equal(validation.ok, false); + assert.deepEqual(validation.errors, [ + 'coding.min_changed_line_coverage must be a number between 0 and 100', + 'review.min_reviewers must be a non-negative number', + ]); + }); +}); + +describe('adaptive intake recommendation', () => { + test('recommends yolo without team notification for low-risk docs work', () => { + const recommendation = buildWorkflowRecommendation({ + title: 'Update README docs', + description: 'Documentation cleanup only', + change_type: 'docs', + estimated_file_count: 1, + }); + + assert.equal(recommendation.recommended_track, 'yolo'); + assert.equal(recommendation.requires_coverage_gate, false); + assert.equal(recommendation.requires_human_checkpoint, false); + assert.equal(recommendation.requires_team_notification, false); + }); + + test('recommends corps, coverage, checkpoint, and team notification for high-risk cross-service work', () => { + const recommendation = buildWorkflowRecommendation({ + title: 'Payment API migration', + description: 'Migrate user payment API and database schema across services', + repositories: ['api', 'billing'], + estimated_file_count: 18, + }, { + execution_mode: 'team', + }); + + assert.equal(recommendation.recommended_track, 'corps'); + assert.equal(recommendation.requires_coverage_gate, true); + assert.equal(recommendation.requires_human_checkpoint, true); + assert.equal(recommendation.requires_team_notification, true); + assert.equal(recommendation.profile.risk_level, 'critical'); + }); +}); + +describe('enterprise integration atoms', () => { + test('policy.gate fails with structured reasons when team thresholds are unmet', async () => { + const projectRoot = makeProjectRoot(); + try { + writeFileSync(resolve(projectRoot, '.xflow', 'profile.json'), JSON.stringify({ execution_mode: 'team' }, null, 2)); + writeFileSync(resolve(projectRoot, '.xflow', 'team-policy.json'), JSON.stringify({ + coding: { min_changed_line_coverage: 80 }, + review: { min_reviewers: 2 }, + deploy: { required_smoke_pass_rate: 95 }, + }, null, 2)); + mkdirSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'tdd'), { recursive: true }); + writeFileSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'tdd', 'quality-0.json'), JSON.stringify({ + ok: true, + changed_line_coverage: 74, + }, null, 2)); + writeFileSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'review.json'), JSON.stringify({ + reviewers: ['alice'], + }, null, 2)); + writeFileSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'smoke.json'), JSON.stringify({ + pass_rate: 90, + }, null, 2)); + + const result = await policyGate({}, { projectRoot, changeId: 'chg-policy-1' }); + + assert.equal(result.ok, false); + assert.deepEqual(result.failed_checks.map((check) => check.code), [ + 'coverage_below_policy', + 'reviewers_below_policy', + 'smoke_pass_rate_below_policy', + ]); + assert.ok(existsSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'policy_gate.json'))); + } finally { + cleanupProjectRoot(projectRoot); + } + }); + + test('notify.checkpoint writes notification evidence but does not approve a human gate', async () => { + const projectRoot = makeProjectRoot(); + try { + const result = await notifyCheckpoint( + { + provider: 'file', + phase: 'tech-plan', + recipient: 'team@example.com', + artifact_url: 'specs/changes/chg-policy-1/plan.md', + }, + { projectRoot, changeId: 'chg-policy-1' }, + ); + + assert.equal(result.ok, true); + assert.equal(result.approves_gate, false); + const evidence = readJson(result.notification_file); + assert.equal(evidence.provider, 'file'); + assert.equal(evidence.phase, 'tech-plan'); + assert.equal(evidence.approves_gate, false); + } finally { + cleanupProjectRoot(projectRoot); + } + }); + + test('plugin.run dry-runs script and webhook plugins into structured results', async () => { + const projectRoot = makeProjectRoot(); + try { + const scriptResult = await pluginRun( + { type: 'script', ref: 'scripts/check.sh', dry_run: true, payload: { hello: 'world' } }, + { projectRoot, changeId: 'chg-policy-1' }, + ); + const webhookResult = await pluginRun( + { type: 'webhook', ref: 'https://example.invalid/hook', dry_run: true }, + { projectRoot, changeId: 'chg-policy-1' }, + ); + + assert.equal(scriptResult.ok, true); + assert.equal(scriptResult.result.status, 'dry_run'); + assert.equal(webhookResult.result.status, 'dry_run'); + assert.ok(existsSync(scriptResult.plugin_result_file)); + } finally { + cleanupProjectRoot(projectRoot); + } + }); + + test('storage.sync file provider copies experience entries and records evidence', async () => { + const projectRoot = makeProjectRoot(); + try { + const source = resolve(projectRoot, 'team-experience.json'); + writeFileSync(source, JSON.stringify({ entries: [{ title: 'Prefer real assertions', category: 'testing' }] }, null, 2)); + + const result = await storageSync( + { provider: 'file', direction: 'import', source, target: 'team/experience.json' }, + { projectRoot, changeId: 'chg-policy-1' }, + ); + + assert.equal(result.ok, true); + assert.ok(existsSync(resolve(projectRoot, '.xflow', 'storage', 'team', 'experience.json'))); + assert.ok(existsSync(result.storage_sync_file)); + } finally { + cleanupProjectRoot(projectRoot); + } + }); + + test('experience.digest summarizes local and team experience as advisory context', async () => { + const projectRoot = makeProjectRoot(); + try { + mkdirSync(resolve(projectRoot, '.xflow', 'experience'), { recursive: true }); + mkdirSync(resolve(projectRoot, '.xflow', 'storage', 'team'), { recursive: true }); + writeFileSync(resolve(projectRoot, '.xflow', 'experience', 'local.json'), JSON.stringify({ + entries: [{ title: 'Keep tests meaningful', category: 'testing', content: 'Reject empty assertions.' }], + }, null, 2)); + writeFileSync(resolve(projectRoot, '.xflow', 'storage', 'team', 'experience.json'), JSON.stringify({ + entries: [{ title: 'Review migrations', category: 'review', content: 'Require migration smoke evidence.' }], + }, null, 2)); + + const result = await experienceDigest({}, { projectRoot, changeId: 'chg-policy-1' }); + + assert.equal(result.ok, true); + assert.equal(result.digest.entry_count, 2); + assert.equal(result.digest.advisory_only, true); + assert.ok(existsSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'experience_digest.json'))); + } finally { + cleanupProjectRoot(projectRoot); + } + }); +}); + +describe('CLI policy/profile/intake surfaces', () => { + test('profile show, policy validate, and intake recommend expose JSON surfaces', () => { + const projectRoot = makeProjectRoot(); + try { + writeFileSync(resolve(projectRoot, '.xflow', 'profile.json'), JSON.stringify({ execution_mode: 'personal' }, null, 2)); + writeFileSync(resolve(projectRoot, '.xflow', 'team-policy.json'), JSON.stringify({ + coding: { min_changed_line_coverage: 80 }, + }, null, 2)); + + const profile = spawnSync('node', [resolve(REPO_ROOT, 'bin/xflow.js'), 'profile', 'show', '--project-root', projectRoot, '--json'], { encoding: 'utf8' }); + assert.equal(profile.status, 0, profile.stderr || profile.stdout); + assert.equal(JSON.parse(profile.stdout).profile.execution_mode, 'personal'); + + const policy = spawnSync('node', [resolve(REPO_ROOT, 'bin/xflow.js'), 'policy', 'validate', '--project-root', projectRoot, '--change-id', 'chg-policy-1', '--json'], { encoding: 'utf8' }); + assert.equal(policy.status, 0, policy.stderr || policy.stdout); + assert.equal(JSON.parse(policy.stdout).ok, true); + + const intake = spawnSync('node', [ + resolve(REPO_ROOT, 'bin/xflow.js'), + 'intake', + 'recommend', + '--project-root', + projectRoot, + '--change-id', + 'chg-policy-1', + '--title', + 'Payment API migration', + '--description', + 'payment user data api database', + '--repo-count', + '2', + '--json', + ], { encoding: 'utf8' }); + assert.equal(intake.status, 0, intake.stderr || intake.stdout); + assert.equal(JSON.parse(intake.stdout).recommendation.recommended_track, 'corps'); + assert.ok(existsSync(resolve(projectRoot, 'specs', 'changes', 'chg-policy-1', 'workflow_recommendation.json'))); + } finally { + cleanupProjectRoot(projectRoot); + } + }); +}); diff --git a/test/release-checklist.test.js b/test/release-checklist.test.js index 5a1716d..7653da9 100644 --- a/test/release-checklist.test.js +++ b/test/release-checklist.test.js @@ -55,6 +55,7 @@ describe('local release checklist', () => { 'docs/integrations.md', 'docs/goal-vs-codex.md', 'docs/superpowers-comparison.md', + 'docs/super-assistant-comparison.md', 'docs/adoption/README.md', 'docs/adoption/openflow-release-hardening.md', 'docs/spec-kit-benchmark.md', diff --git a/test/workflow-manual-current.test.js b/test/workflow-manual-current.test.js index ea7dbe1..fd381f3 100644 --- a/test/workflow-manual-current.test.js +++ b/test/workflow-manual-current.test.js @@ -11,8 +11,10 @@ describe('workflow manual and handoff currentness', () => { const atomCount = Object.keys(registry.atoms).length; const manual = readFileSync(resolve(REPO_ROOT, 'docs', 'workflow-manual.md'), 'utf8'); - assert.equal(atomCount, 76); - assert.match(manual, /76 atoms/); + assert.equal(atomCount, 83); + assert.match(manual, /83 atoms/); + assert.doesNotMatch(manual, /82 atoms/); + assert.doesNotMatch(manual, /76 atoms/); assert.doesNotMatch(manual, /65 atoms/); assert.doesNotMatch(manual, /64 atoms/); assert.doesNotMatch(manual, /63 atoms/);