@ai-content-space/loopx 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -6
- package/README.zh-CN.md +143 -10
- package/assets/logo.svg +89 -0
- package/package.json +4 -2
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +13 -0
- package/plugins/loopx/skills/archive/SKILL.md +14 -1
- package/plugins/loopx/skills/autopilot/SKILL.md +4 -1
- package/plugins/loopx/skills/build/SKILL.md +7 -1
- package/plugins/loopx/skills/clarify/SKILL.md +13 -9
- package/plugins/loopx/skills/debug/SKILL.md +4 -1
- package/plugins/loopx/skills/go-style/SKILL.md +4 -1
- package/plugins/loopx/skills/kratos/SKILL.md +4 -1
- package/plugins/loopx/skills/plan/SKILL.md +8 -4
- package/plugins/loopx/skills/review/SKILL.md +7 -1
- package/plugins/loopx/skills/tdd/SKILL.md +4 -1
- package/plugins/loopx/skills/verify/SKILL.md +4 -1
- package/scripts/codex-workflow-hook.mjs +101 -6
- package/scripts/verify-skills.mjs +166 -0
- package/skills/RESOLVER.md +45 -0
- package/skills/archive/SKILL.md +14 -1
- package/skills/autopilot/SKILL.md +4 -1
- package/skills/build/SKILL.md +7 -1
- package/skills/clarify/SKILL.md +13 -9
- package/skills/debug/SKILL.md +4 -1
- package/skills/go-style/SKILL.md +4 -1
- package/skills/kratos/SKILL.md +4 -1
- package/skills/plan/SKILL.md +8 -4
- package/skills/review/SKILL.md +7 -1
- package/skills/tdd/SKILL.md +4 -1
- package/skills/verify/SKILL.md +4 -1
- package/src/build-runtime.mjs +8 -0
- package/src/cli.mjs +10 -0
- package/src/context-manifest.mjs +3 -1
- package/src/html-views.mjs +316 -0
- package/src/plan-runtime.mjs +23 -0
- package/src/project-discovery.mjs +163 -0
- package/src/review-runtime.mjs +203 -23
- package/src/runtime-maintenance.mjs +1 -0
- package/src/workflow.mjs +499 -94
package/src/workflow.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { doctorRuntime, ensureLoopxRoot, resolveLoopxRoot } from './runtime-maintenance.mjs';
|
|
17
17
|
import { DEFAULT_BUILD_MAX_ITERATIONS, createDefaultBuildAdapter } from './build-runtime.mjs';
|
|
18
18
|
import { DEFAULT_MAX_ITERATIONS, createDefaultPlanAdapter } from './plan-runtime.mjs';
|
|
19
|
+
import { inspectProjectConventions } from './project-discovery.mjs';
|
|
19
20
|
import { createDefaultReviewAdapter } from './review-runtime.mjs';
|
|
20
21
|
import { appendWorkspaceJournal } from './workspace-memory.mjs';
|
|
21
22
|
import { inspectWorkspaceContext, setupWorkspaceContext } from './workspace-context.mjs';
|
|
@@ -251,6 +252,10 @@ function resolveSpecsRoot(cwd) {
|
|
|
251
252
|
return join(resolveWorkspaceRoot(cwd), 'specs');
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
function resolveIntakeRoot(cwd) {
|
|
256
|
+
return join(resolveWorkspaceRoot(cwd), 'intake');
|
|
257
|
+
}
|
|
258
|
+
|
|
254
259
|
function resolveChangesRoot(cwd) {
|
|
255
260
|
return join(resolveWorkspaceRoot(cwd), 'changes');
|
|
256
261
|
}
|
|
@@ -299,7 +304,7 @@ function resolveBuildSupportPaths(root, iteration) {
|
|
|
299
304
|
}
|
|
300
305
|
|
|
301
306
|
function canonicalClarifySpecPath(cwd, slug, stamp) {
|
|
302
|
-
return join(
|
|
307
|
+
return join(resolveIntakeRoot(cwd), `clarify-${normalizeSlug(slug)}-${stamp}.md`);
|
|
303
308
|
}
|
|
304
309
|
|
|
305
310
|
export async function readWorkspaceConfig(cwd) {
|
|
@@ -336,11 +341,39 @@ function buildWorkspaceReadme() {
|
|
|
336
341
|
'- `loopx plan <slug>`',
|
|
337
342
|
'- `loopx build <slug>`',
|
|
338
343
|
'- `loopx review <slug> [--reviewer <name>]`',
|
|
344
|
+
'- `loopx archive <slug>`',
|
|
339
345
|
'- `loopx autopilot <slug> [--reviewer <name>]`',
|
|
346
|
+
'- `loopx render [slug|--all]`',
|
|
340
347
|
'- `loopx status [slug] [--json]`',
|
|
348
|
+
'- `loopx setup-context`',
|
|
341
349
|
'- `loopx doctor`',
|
|
342
350
|
'- `loopx migrate`',
|
|
343
351
|
'- `loopx repair-install`',
|
|
352
|
+
'',
|
|
353
|
+
'## Document Boundaries',
|
|
354
|
+
'',
|
|
355
|
+
'User-facing documents to watch:',
|
|
356
|
+
'',
|
|
357
|
+
'- `workflows/<slug>/spec.md`',
|
|
358
|
+
'- `workflows/<slug>/plan.md`, `architecture.md`, `development-plan.md`, and `test-plan.md`',
|
|
359
|
+
'- `workflows/<slug>/execution-record.md` and `review-report.md`',
|
|
360
|
+
'- `views/index.html` and `workflows/<slug>/view/index.html` after `loopx render`',
|
|
361
|
+
'',
|
|
362
|
+
'Documents users may read and edit as workflow fact sources:',
|
|
363
|
+
'',
|
|
364
|
+
'- `workflows/<slug>/*.md` for the active workflow working copy',
|
|
365
|
+
'- `context/domain.md` and `agents/*.md` for project context and collaboration guidance',
|
|
366
|
+
'- `changes/active/<change-id>/*.md` for proposal, design, tasks, and spec delta',
|
|
367
|
+
'- `specs/<domain>/spec.md` for archived long-lived behavior specs',
|
|
368
|
+
'',
|
|
369
|
+
'Tool-owned or derived files:',
|
|
370
|
+
'',
|
|
371
|
+
'- `workflows/<slug>/state.json`, `build-context.jsonl`, and `review-context.jsonl`',
|
|
372
|
+
'- `workflows/<slug>/plan-reviews/`, `build-support/`, and `review-support/`',
|
|
373
|
+
'- `intake/clarify-*.md` clarify snapshots',
|
|
374
|
+
'- `changes/active/<change-id>/slices.json` and `artifact-graph.json`',
|
|
375
|
+
'- `autopilot/<slug>/run.json` and `build-active.json`',
|
|
376
|
+
'- `views/` and `workflows/<slug>/view/` generated HTML views',
|
|
344
377
|
].join('\n');
|
|
345
378
|
}
|
|
346
379
|
|
|
@@ -384,6 +417,7 @@ function createInitialState(slug, profile) {
|
|
|
384
417
|
plan_docs_status: 'missing',
|
|
385
418
|
plan_docs_artifact_paths: null,
|
|
386
419
|
plan_review_artifact_paths: [],
|
|
420
|
+
plan_review_history: [],
|
|
387
421
|
plan_blockers: [],
|
|
388
422
|
plan_source_spec_path: null,
|
|
389
423
|
change_id: changeIdForWorkflowSlug(slug),
|
|
@@ -576,28 +610,217 @@ function targetDomainsForChange(slug, sourceText) {
|
|
|
576
610
|
return ['general'];
|
|
577
611
|
}
|
|
578
612
|
|
|
579
|
-
function
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
613
|
+
function declaredTargetDomainsForDelta(sourceText) {
|
|
614
|
+
const explicit = bulletsFromSectionText(sourceText, 'Target Spec Domains');
|
|
615
|
+
if (explicit.length > 0) {
|
|
616
|
+
return dedupeStrings(explicit.map((item) => item.replace(/`/g, '')));
|
|
617
|
+
}
|
|
618
|
+
const frontmatterDomains = frontmatterList(sourceText, 'target_domains');
|
|
619
|
+
if (frontmatterDomains.length > 0) {
|
|
620
|
+
return dedupeStrings(frontmatterDomains.map((item) => item.replace(/`/g, '')));
|
|
621
|
+
}
|
|
622
|
+
return [];
|
|
583
623
|
}
|
|
584
624
|
|
|
585
|
-
function
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
625
|
+
function stripFrontmatter(text) {
|
|
626
|
+
if (!text.startsWith('---\n')) {
|
|
627
|
+
return text;
|
|
628
|
+
}
|
|
629
|
+
const end = text.indexOf('\n---\n', 4);
|
|
630
|
+
return end === -1 ? text : text.slice(end + 5);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function normalizeRequirementName(raw) {
|
|
634
|
+
return String(raw || '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function requirementDisplayName(raw) {
|
|
638
|
+
return String(raw || '').trim().replace(/\s+/g, ' ');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function sentenceToRequirementName(text, fallback) {
|
|
642
|
+
const cleaned = String(text || '')
|
|
643
|
+
.replace(/[`*_#]/g, '')
|
|
644
|
+
.replace(/\s+/g, ' ')
|
|
645
|
+
.trim()
|
|
646
|
+
.replace(/[.。::]+$/, '');
|
|
647
|
+
if (!cleaned) {
|
|
648
|
+
return fallback;
|
|
649
|
+
}
|
|
650
|
+
const withoutModal = cleaned
|
|
651
|
+
.replace(/\bSHALL\b.*$/i, '')
|
|
652
|
+
.replace(/\bMUST\b.*$/i, '')
|
|
653
|
+
.trim();
|
|
654
|
+
const value = withoutModal || cleaned;
|
|
655
|
+
return value.length > 80 ? value.slice(0, 77).trim() : value;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function normativeRequirementText(text, slug, index) {
|
|
659
|
+
const cleaned = String(text || '').replace(/\s+/g, ' ').trim().replace(/[.。]+$/, '');
|
|
660
|
+
if (/\b(SHALL|MUST)\b/i.test(cleaned)) {
|
|
661
|
+
return `${cleaned}.`;
|
|
662
|
+
}
|
|
663
|
+
return `Workflow ${slug} SHALL satisfy: ${cleaned || `approved requirement ${index + 1}`}.`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function scenarioNameForRequirement(name) {
|
|
667
|
+
const cleaned = requirementDisplayName(name).replace(/[.。]+$/, '');
|
|
668
|
+
return cleaned.length > 70 ? cleaned.slice(0, 67).trim() : cleaned;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function requirementBlockFromText({ slug, text, index }) {
|
|
672
|
+
const name = sentenceToRequirementName(text, `Approved requirement ${index + 1}`);
|
|
673
|
+
return [
|
|
674
|
+
`### Requirement: ${name}`,
|
|
675
|
+
normativeRequirementText(text, slug, index),
|
|
676
|
+
'',
|
|
677
|
+
`#### Scenario: ${scenarioNameForRequirement(name)}`,
|
|
678
|
+
`- GIVEN workflow ${slug} has an approved plan`,
|
|
679
|
+
`- WHEN the accepted implementation is archived`,
|
|
680
|
+
`- THEN the system satisfies: ${String(text || '').replace(/\s+/g, ' ').trim() || name}`,
|
|
681
|
+
].join('\n');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function splitDeltaSections(text) {
|
|
685
|
+
const body = stripFrontmatter(text);
|
|
686
|
+
const pattern = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/gim;
|
|
687
|
+
const matches = [...body.matchAll(pattern)];
|
|
688
|
+
const sections = new Map();
|
|
689
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
690
|
+
const match = matches[index];
|
|
691
|
+
const kind = match[1].toUpperCase();
|
|
692
|
+
const start = match.index + match[0].length;
|
|
693
|
+
const end = index + 1 < matches.length ? matches[index + 1].index : body.length;
|
|
694
|
+
sections.set(kind, body.slice(start, end).trim());
|
|
695
|
+
}
|
|
696
|
+
return sections;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function parseRequirementBlocks(sectionText) {
|
|
700
|
+
const pattern = /^###\s+Requirement:\s*(.+?)\s*$/gm;
|
|
701
|
+
const matches = [...String(sectionText || '').matchAll(pattern)];
|
|
702
|
+
return matches.map((match, index) => {
|
|
703
|
+
const start = match.index;
|
|
704
|
+
const end = index + 1 < matches.length ? matches[index + 1].index : sectionText.length;
|
|
705
|
+
return {
|
|
706
|
+
name: requirementDisplayName(match[1]),
|
|
707
|
+
raw: sectionText.slice(start, end).trim(),
|
|
708
|
+
};
|
|
709
|
+
}).filter((block) => block.name && block.raw);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function parseRenamedRequirement(block) {
|
|
713
|
+
const inline = block.name.match(/^(.*?)\s*(?:->|=>)\s*(.*?)$/);
|
|
714
|
+
if (inline) {
|
|
715
|
+
return {
|
|
716
|
+
from: requirementDisplayName(inline[1]),
|
|
717
|
+
to: requirementDisplayName(inline[2]),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const from = block.raw.match(/^FROM:\s*(.+?)\s*$/im)?.[1];
|
|
721
|
+
const to = block.raw.match(/^TO:\s*(.+?)\s*$/im)?.[1];
|
|
722
|
+
return {
|
|
723
|
+
from: requirementDisplayName(from || block.name),
|
|
724
|
+
to: requirementDisplayName(to || ''),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function countRequirementScenarios(raw) {
|
|
729
|
+
return (String(raw || '').match(/^####\s+Scenario:\s*.+$/gim) || []).length;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function requirementTextBeforeScenarios(raw) {
|
|
733
|
+
const lines = String(raw || '').split('\n').slice(1);
|
|
734
|
+
const scenarioIndex = lines.findIndex((line) => /^####\s+Scenario:/i.test(line.trim()));
|
|
735
|
+
const requirementLines = scenarioIndex === -1 ? lines : lines.slice(0, scenarioIndex);
|
|
736
|
+
return requirementLines.map((line) => line.trim()).filter(Boolean).join(' ');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function parseRequirementDelta(text) {
|
|
740
|
+
const sections = splitDeltaSections(text);
|
|
741
|
+
const added = parseRequirementBlocks(sections.get('ADDED') || '');
|
|
742
|
+
const modified = parseRequirementBlocks(sections.get('MODIFIED') || '');
|
|
743
|
+
const removed = parseRequirementBlocks(sections.get('REMOVED') || '').map((block) => block.name);
|
|
744
|
+
const renamed = parseRequirementBlocks(sections.get('RENAMED') || '').map(parseRenamedRequirement);
|
|
745
|
+
return { added, modified, removed, renamed };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function deltaOperationCount(delta) {
|
|
749
|
+
return delta.added.length + delta.modified.length + delta.removed.length + delta.renamed.length;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function validateRequirementDelta(text) {
|
|
753
|
+
const delta = parseRequirementDelta(text);
|
|
754
|
+
const blockers = [];
|
|
755
|
+
if (deltaOperationCount(delta) === 0) {
|
|
756
|
+
blockers.push('spec_delta_missing_requirement_operations');
|
|
757
|
+
return { delta, blockers };
|
|
758
|
+
}
|
|
759
|
+
const seenBySection = {
|
|
760
|
+
added: new Set(),
|
|
761
|
+
modified: new Set(),
|
|
762
|
+
removed: new Set(),
|
|
763
|
+
renamedFrom: new Set(),
|
|
764
|
+
renamedTo: new Set(),
|
|
765
|
+
};
|
|
766
|
+
for (const [section, blocks] of [['added', delta.added], ['modified', delta.modified]]) {
|
|
767
|
+
for (const block of blocks) {
|
|
768
|
+
const key = normalizeRequirementName(block.name);
|
|
769
|
+
if (seenBySection[section].has(key)) {
|
|
770
|
+
blockers.push(`spec_delta_duplicate_${section}_${key}`);
|
|
771
|
+
}
|
|
772
|
+
seenBySection[section].add(key);
|
|
773
|
+
const requirementText = requirementTextBeforeScenarios(block.raw);
|
|
774
|
+
if (!requirementText) {
|
|
775
|
+
blockers.push(`spec_delta_${section}_${key}_missing_text`);
|
|
776
|
+
}
|
|
777
|
+
if (!/\b(SHALL|MUST)\b/i.test(requirementText)) {
|
|
778
|
+
blockers.push(`spec_delta_${section}_${key}_missing_shall_must`);
|
|
779
|
+
}
|
|
780
|
+
if (countRequirementScenarios(block.raw) === 0) {
|
|
781
|
+
blockers.push(`spec_delta_${section}_${key}_missing_scenario`);
|
|
782
|
+
}
|
|
592
783
|
}
|
|
593
|
-
entries.set(domain, {
|
|
594
|
-
added: bulletsFromSectionText(domainText, 'Added Requirements').filter((item) => item !== 'none'),
|
|
595
|
-
modified: bulletsFromSectionText(domainText, 'Modified Requirements').filter((item) => item !== 'none'),
|
|
596
|
-
removed: bulletsFromSectionText(domainText, 'Removed Requirements').filter((item) => item !== 'none'),
|
|
597
|
-
scenarios: bulletsFromSectionText(domainText, 'Scenarios'),
|
|
598
|
-
});
|
|
599
784
|
}
|
|
600
|
-
|
|
785
|
+
for (const name of delta.removed) {
|
|
786
|
+
const key = normalizeRequirementName(name);
|
|
787
|
+
if (seenBySection.removed.has(key)) {
|
|
788
|
+
blockers.push(`spec_delta_duplicate_removed_${key}`);
|
|
789
|
+
}
|
|
790
|
+
seenBySection.removed.add(key);
|
|
791
|
+
}
|
|
792
|
+
for (const item of delta.renamed) {
|
|
793
|
+
const from = normalizeRequirementName(item.from);
|
|
794
|
+
const to = normalizeRequirementName(item.to);
|
|
795
|
+
if (!from || !to) {
|
|
796
|
+
blockers.push('spec_delta_renamed_missing_from_or_to');
|
|
797
|
+
}
|
|
798
|
+
if (seenBySection.renamedFrom.has(from)) {
|
|
799
|
+
blockers.push(`spec_delta_duplicate_renamed_from_${from}`);
|
|
800
|
+
}
|
|
801
|
+
if (seenBySection.renamedTo.has(to)) {
|
|
802
|
+
blockers.push(`spec_delta_duplicate_renamed_to_${to}`);
|
|
803
|
+
}
|
|
804
|
+
seenBySection.renamedFrom.add(from);
|
|
805
|
+
seenBySection.renamedTo.add(to);
|
|
806
|
+
}
|
|
807
|
+
for (const name of seenBySection.added) {
|
|
808
|
+
if (seenBySection.modified.has(name)) {
|
|
809
|
+
blockers.push(`spec_delta_conflict_added_modified_${name}`);
|
|
810
|
+
}
|
|
811
|
+
if (seenBySection.removed.has(name)) {
|
|
812
|
+
blockers.push(`spec_delta_conflict_added_removed_${name}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
for (const name of seenBySection.modified) {
|
|
816
|
+
if (seenBySection.removed.has(name)) {
|
|
817
|
+
blockers.push(`spec_delta_conflict_modified_removed_${name}`);
|
|
818
|
+
}
|
|
819
|
+
if (seenBySection.renamedFrom.has(name)) {
|
|
820
|
+
blockers.push(`spec_delta_conflict_modified_renamed_from_${name}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return { delta, blockers: dedupeStrings(blockers) };
|
|
601
824
|
}
|
|
602
825
|
|
|
603
826
|
function requirementsForDelta(slug, plannerDraft) {
|
|
@@ -724,29 +947,18 @@ async function writeChangeArtifacts(cwd, root, slug, sourceText, plannerDraft, c
|
|
|
724
947
|
].join('\n'));
|
|
725
948
|
|
|
726
949
|
await writeText(paths.specDelta, [
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
'',
|
|
731
|
-
...domains.map((domain) =>
|
|
732
|
-
'',
|
|
733
|
-
'## Added Requirements',
|
|
734
|
-
'',
|
|
735
|
-
...requirements.map((item) => `- ${item}`),
|
|
736
|
-
'',
|
|
737
|
-
'## Modified Requirements',
|
|
950
|
+
'---',
|
|
951
|
+
`change_id: ${normalizedChangeId}`,
|
|
952
|
+
`slug: ${slug}`,
|
|
953
|
+
'target_domains:',
|
|
954
|
+
...domains.map((domain) => ` - ${domain}`),
|
|
955
|
+
'---',
|
|
738
956
|
'',
|
|
739
|
-
|
|
740
|
-
'',
|
|
741
|
-
'## Removed Requirements',
|
|
742
|
-
'',
|
|
743
|
-
'- none',
|
|
957
|
+
`# loopx Spec Delta: ${normalizedChangeId}`,
|
|
744
958
|
'',
|
|
745
|
-
'##
|
|
959
|
+
'## ADDED Requirements',
|
|
746
960
|
'',
|
|
747
|
-
|
|
748
|
-
'- WHEN build and review complete successfully',
|
|
749
|
-
'- THEN the accepted behavior is merged into long-lived loopx specs during archive',
|
|
961
|
+
...requirements.flatMap((item, index) => [requirementBlockFromText({ slug, text: item, index }), '']),
|
|
750
962
|
].join('\n'));
|
|
751
963
|
|
|
752
964
|
await writeText(paths.design, [
|
|
@@ -805,26 +1017,43 @@ async function readChangeArtifactStatus(paths) {
|
|
|
805
1017
|
let specDeltaStatus = 'missing';
|
|
806
1018
|
if (paths.specDelta && existsSync(paths.specDelta)) {
|
|
807
1019
|
const text = await readFile(paths.specDelta, 'utf8');
|
|
808
|
-
const parsedDelta =
|
|
809
|
-
const
|
|
810
|
-
const hasDomains =
|
|
811
|
-
const hasRequirements = parsedDelta.added.length > 0
|
|
812
|
-
|| parsedDelta.modified.length > 0
|
|
813
|
-
|| domainDeltas.some((delta) => delta.added.length > 0 || delta.modified.length > 0);
|
|
1020
|
+
const parsedDelta = validateRequirementDelta(text);
|
|
1021
|
+
const declaredDomains = declaredTargetDomainsForDelta(text);
|
|
1022
|
+
const hasDomains = declaredDomains.length > 0;
|
|
814
1023
|
if (!text.trim()) {
|
|
815
1024
|
specDeltaStatus = 'partial';
|
|
816
1025
|
blockers.push('spec_delta_empty');
|
|
817
|
-
} else if (!hasDomains ||
|
|
1026
|
+
} else if (!hasDomains || parsedDelta.blockers.length > 0) {
|
|
818
1027
|
specDeltaStatus = 'partial';
|
|
819
1028
|
if (!hasDomains) {
|
|
820
1029
|
blockers.push('spec_delta_missing_domains');
|
|
821
1030
|
}
|
|
822
|
-
|
|
823
|
-
blockers.push('spec_delta_missing_requirements');
|
|
824
|
-
}
|
|
1031
|
+
blockers.push(...parsedDelta.blockers);
|
|
825
1032
|
} else {
|
|
826
1033
|
specDeltaStatus = 'complete';
|
|
827
1034
|
}
|
|
1035
|
+
const specsRoot = paths.root ? join(paths.root, 'specs') : null;
|
|
1036
|
+
if (specsRoot && existsSync(specsRoot)) {
|
|
1037
|
+
const entries = await readdir(specsRoot, { withFileTypes: true });
|
|
1038
|
+
const declaredDomainSet = new Set(declaredDomains);
|
|
1039
|
+
for (const entry of entries) {
|
|
1040
|
+
if (!entry.isDirectory() || declaredDomainSet.has(entry.name)) {
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const candidate = join(specsRoot, entry.name, 'spec.md');
|
|
1044
|
+
if (!existsSync(candidate)) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const domainDelta = await readFile(candidate, 'utf8');
|
|
1048
|
+
const validation = validateRequirementDelta(domainDelta);
|
|
1049
|
+
if (validation.blockers.length > 0) {
|
|
1050
|
+
specDeltaStatus = 'partial';
|
|
1051
|
+
blockers.push(
|
|
1052
|
+
...validation.blockers.map((blocker) => `spec_delta_${entry.name}_${blocker.replace(/^spec_delta_/, '')}`),
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
828
1057
|
}
|
|
829
1058
|
if (paths.slices && existsSync(paths.slices)) {
|
|
830
1059
|
try {
|
|
@@ -891,14 +1120,10 @@ async function ensureArchiveSlicesArtifact(cwd, root, slug, state) {
|
|
|
891
1120
|
}
|
|
892
1121
|
|
|
893
1122
|
function parseSpecDelta(text) {
|
|
894
|
-
const
|
|
1123
|
+
const parsed = parseRequirementDelta(text);
|
|
895
1124
|
return {
|
|
896
|
-
domains:
|
|
897
|
-
|
|
898
|
-
modified: bulletsFromSectionText(text, 'Modified Requirements').filter((item) => item !== 'none'),
|
|
899
|
-
removed: bulletsFromSectionText(text, 'Removed Requirements').filter((item) => item !== 'none'),
|
|
900
|
-
scenarios: bulletsFromSectionText(text, 'Scenarios'),
|
|
901
|
-
domainDeltas,
|
|
1125
|
+
domains: declaredTargetDomainsForDelta(text),
|
|
1126
|
+
...parsed,
|
|
902
1127
|
};
|
|
903
1128
|
}
|
|
904
1129
|
|
|
@@ -942,28 +1167,146 @@ async function writeAdrCandidate(cwd, changeId, state, archivedSpecPaths) {
|
|
|
942
1167
|
return path;
|
|
943
1168
|
}
|
|
944
1169
|
|
|
945
|
-
function
|
|
946
|
-
|
|
947
|
-
|
|
1170
|
+
function splitSpecRequirements(existing) {
|
|
1171
|
+
const text = String(existing || '');
|
|
1172
|
+
const match = text.match(/^##\s+Requirements\s*$/im);
|
|
1173
|
+
if (!match) {
|
|
1174
|
+
return {
|
|
1175
|
+
before: text.trimEnd(),
|
|
1176
|
+
header: '## Requirements',
|
|
1177
|
+
body: '',
|
|
1178
|
+
after: '',
|
|
1179
|
+
};
|
|
948
1180
|
}
|
|
949
|
-
const
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
1181
|
+
const headerStart = match.index;
|
|
1182
|
+
const bodyStart = headerStart + match[0].length;
|
|
1183
|
+
const rest = text.slice(bodyStart);
|
|
1184
|
+
const nextTopHeading = rest.search(/\n##\s+/);
|
|
1185
|
+
const body = nextTopHeading === -1 ? rest : rest.slice(0, nextTopHeading);
|
|
1186
|
+
const after = nextTopHeading === -1 ? '' : rest.slice(nextTopHeading);
|
|
1187
|
+
return {
|
|
1188
|
+
before: text.slice(0, headerStart).trimEnd(),
|
|
1189
|
+
header: match[0],
|
|
1190
|
+
body: body.trim(),
|
|
1191
|
+
after: after.trimEnd(),
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function requirementMapFromSpec(existing) {
|
|
1196
|
+
const parts = splitSpecRequirements(existing);
|
|
1197
|
+
const blocks = parseRequirementBlocks(parts.body);
|
|
1198
|
+
const map = new Map();
|
|
1199
|
+
const order = [];
|
|
1200
|
+
for (const block of blocks) {
|
|
1201
|
+
const key = normalizeRequirementName(block.name);
|
|
1202
|
+
if (!map.has(key)) {
|
|
1203
|
+
order.push(key);
|
|
1204
|
+
}
|
|
1205
|
+
map.set(key, block);
|
|
953
1206
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1207
|
+
return { parts, map, order };
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function applyRequirementDelta(existing, delta, domain) {
|
|
1211
|
+
const { parts, map, order } = requirementMapFromSpec(existing);
|
|
1212
|
+
const ensureExisting = (key, label, name) => {
|
|
1213
|
+
if (!map.has(key)) {
|
|
1214
|
+
throw new Error(`${domain} ${label} failed for "### Requirement: ${name}" - not found`);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
for (const item of delta.renamed) {
|
|
1219
|
+
const fromKey = normalizeRequirementName(item.from);
|
|
1220
|
+
const toKey = normalizeRequirementName(item.to);
|
|
1221
|
+
ensureExisting(fromKey, 'RENAMED', item.from);
|
|
1222
|
+
if (map.has(toKey)) {
|
|
1223
|
+
throw new Error(`${domain} RENAMED failed for "### Requirement: ${item.to}" - target already exists`);
|
|
1224
|
+
}
|
|
1225
|
+
const block = map.get(fromKey);
|
|
1226
|
+
const rawLines = block.raw.split('\n');
|
|
1227
|
+
rawLines[0] = `### Requirement: ${item.to}`;
|
|
1228
|
+
map.delete(fromKey);
|
|
1229
|
+
map.set(toKey, { name: item.to, raw: rawLines.join('\n') });
|
|
1230
|
+
const orderIndex = order.indexOf(fromKey);
|
|
1231
|
+
if (orderIndex !== -1) {
|
|
1232
|
+
order[orderIndex] = toKey;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
for (const name of delta.removed) {
|
|
1237
|
+
const key = normalizeRequirementName(name);
|
|
1238
|
+
ensureExisting(key, 'REMOVED', name);
|
|
1239
|
+
map.delete(key);
|
|
1240
|
+
const orderIndex = order.indexOf(key);
|
|
1241
|
+
if (orderIndex !== -1) {
|
|
1242
|
+
order.splice(orderIndex, 1);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
for (const block of delta.modified) {
|
|
1247
|
+
const key = normalizeRequirementName(block.name);
|
|
1248
|
+
ensureExisting(key, 'MODIFIED', block.name);
|
|
1249
|
+
map.set(key, block);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
for (const block of delta.added) {
|
|
1253
|
+
const key = normalizeRequirementName(block.name);
|
|
1254
|
+
if (map.has(key)) {
|
|
1255
|
+
if (map.get(key).raw.trim() === block.raw.trim()) {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
throw new Error(`${domain} ADDED failed for "### Requirement: ${block.name}" - already exists`);
|
|
1259
|
+
}
|
|
1260
|
+
map.set(key, block);
|
|
1261
|
+
order.push(key);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const requirementBody = order.map((key) => map.get(key)?.raw).filter(Boolean).join('\n\n').trimEnd();
|
|
1265
|
+
return [
|
|
1266
|
+
parts.before,
|
|
1267
|
+
parts.header,
|
|
1268
|
+
requirementBody,
|
|
1269
|
+
parts.after,
|
|
1270
|
+
].filter((part) => String(part || '').trim()).join('\n\n').replace(/\n{3,}/g, '\n\n');
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
async function specDeltaFilesForArchive(cwd, specDeltaPath) {
|
|
1274
|
+
const changeRoot = dirname(specDeltaPath);
|
|
1275
|
+
const mainText = await readFile(specDeltaPath, 'utf8');
|
|
1276
|
+
const files = new Map();
|
|
1277
|
+
const declaredDomains = declaredTargetDomainsForDelta(mainText);
|
|
1278
|
+
if (declaredDomains.length === 0) {
|
|
1279
|
+
throw new Error('archive_blocked:spec_delta_missing_domains');
|
|
1280
|
+
}
|
|
1281
|
+
for (const domain of declaredDomains) {
|
|
1282
|
+
files.set(domain, specDeltaPath);
|
|
1283
|
+
}
|
|
1284
|
+
const specsRoot = join(changeRoot, 'specs');
|
|
1285
|
+
if (existsSync(specsRoot)) {
|
|
1286
|
+
const entries = await readdir(specsRoot, { withFileTypes: true });
|
|
1287
|
+
for (const entry of entries) {
|
|
1288
|
+
if (!entry.isDirectory()) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const candidate = join(specsRoot, entry.name, 'spec.md');
|
|
1292
|
+
if (existsSync(candidate) && !files.has(entry.name)) {
|
|
1293
|
+
files.set(entry.name, candidate);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return files;
|
|
959
1298
|
}
|
|
960
1299
|
|
|
961
1300
|
async function mergeSpecDeltaIntoLongLivedSpecs(cwd, slug, specDeltaPath) {
|
|
962
|
-
const
|
|
963
|
-
const delta = parseSpecDelta(deltaText);
|
|
1301
|
+
const deltaFiles = await specDeltaFilesForArchive(cwd, specDeltaPath);
|
|
964
1302
|
const updated = [];
|
|
965
|
-
for (const domain of
|
|
966
|
-
const
|
|
1303
|
+
for (const [domain, deltaPath] of deltaFiles.entries()) {
|
|
1304
|
+
const deltaText = await readFile(deltaPath, 'utf8');
|
|
1305
|
+
const validation = validateRequirementDelta(deltaText);
|
|
1306
|
+
if (validation.blockers.length > 0) {
|
|
1307
|
+
throw new Error(`archive_blocked:${domain}:${validation.blockers.join(',')}`);
|
|
1308
|
+
}
|
|
1309
|
+
const domainDelta = parseSpecDelta(deltaText);
|
|
967
1310
|
const path = specDomainPath(cwd, domain);
|
|
968
1311
|
await ensureDir(dirname(path));
|
|
969
1312
|
const existing = await readTextIfExists(path);
|
|
@@ -976,15 +1319,7 @@ async function mergeSpecDeltaIntoLongLivedSpecs(cwd, slug, specDeltaPath) {
|
|
|
976
1319
|
'',
|
|
977
1320
|
'## Requirements',
|
|
978
1321
|
].join('\n');
|
|
979
|
-
const
|
|
980
|
-
`### Change: ${slug}`,
|
|
981
|
-
'',
|
|
982
|
-
...(domainDelta.added.length > 0 ? ['#### Added Requirements', '', ...domainDelta.added.map((item) => `- ${item}`), ''] : []),
|
|
983
|
-
...(domainDelta.modified.length > 0 ? ['#### Modified Requirements', '', ...domainDelta.modified.map((item) => `- ${item}`), ''] : []),
|
|
984
|
-
...(domainDelta.removed.length > 0 ? ['#### Removed Requirements', '', ...domainDelta.removed.map((item) => `- ${item}`), ''] : []),
|
|
985
|
-
...(domainDelta.scenarios.length > 0 ? ['#### Scenarios', '', ...domainDelta.scenarios.map((item) => `- ${item}`)] : []),
|
|
986
|
-
].join('\n');
|
|
987
|
-
const next = replaceChangeBlock(base, slug, changeBlock);
|
|
1322
|
+
const next = applyRequirementDelta(base, domainDelta, domain);
|
|
988
1323
|
await writeText(path, next);
|
|
989
1324
|
updated.push(path);
|
|
990
1325
|
}
|
|
@@ -1109,6 +1444,34 @@ async function writePlanReviewArtifacts(root, iteration, plannerDraft, architect
|
|
|
1109
1444
|
return paths;
|
|
1110
1445
|
}
|
|
1111
1446
|
|
|
1447
|
+
function planReviewSummary(iteration, architectReview, criticReview) {
|
|
1448
|
+
return {
|
|
1449
|
+
iteration,
|
|
1450
|
+
architectReview: {
|
|
1451
|
+
status: architectReview.status,
|
|
1452
|
+
verdict: architectReview.verdict,
|
|
1453
|
+
findings: Array.isArray(architectReview.findings) ? architectReview.findings : [],
|
|
1454
|
+
strongestObjection: architectReview.strongestObjection || null,
|
|
1455
|
+
tradeoffTension: architectReview.tradeoffTension || null,
|
|
1456
|
+
},
|
|
1457
|
+
criticReview: {
|
|
1458
|
+
verdict: criticReview.verdict,
|
|
1459
|
+
findings: Array.isArray(criticReview.findings) ? criticReview.findings : [],
|
|
1460
|
+
acceptanceCriteriaTestable: Boolean(criticReview.acceptanceCriteriaTestable),
|
|
1461
|
+
verificationStepsResolved: Boolean(criticReview.verificationStepsResolved),
|
|
1462
|
+
executionInputsResolved: Boolean(criticReview.executionInputsResolved),
|
|
1463
|
+
},
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function initialPlanReviewHistory(state) {
|
|
1468
|
+
const history = Array.isArray(state.plan_review_history) ? state.plan_review_history : [];
|
|
1469
|
+
if (state.current_stage !== STAGES.PLAN || state.stage_status !== 'blocked' || history.length === 0) {
|
|
1470
|
+
return [];
|
|
1471
|
+
}
|
|
1472
|
+
return [history[history.length - 1]];
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1112
1475
|
async function readPlanCompletion(cwd, root, slug, state) {
|
|
1113
1476
|
const blockers = [];
|
|
1114
1477
|
if (state.plan_architect_review_status !== 'complete') {
|
|
@@ -1260,6 +1623,10 @@ async function readJsonIfExists(path) {
|
|
|
1260
1623
|
|
|
1261
1624
|
async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifactPath = null, iterationData, ledger, baseBlockers }) {
|
|
1262
1625
|
const checklist = [];
|
|
1626
|
+
const iterationEvidence = [
|
|
1627
|
+
...(iterationData.executionEvidence || []),
|
|
1628
|
+
...(iterationData.verificationEvidence || []),
|
|
1629
|
+
].filter(Boolean).map(String);
|
|
1263
1630
|
const addChecklistItem = (item) => {
|
|
1264
1631
|
checklist.push({
|
|
1265
1632
|
status: 'covered',
|
|
@@ -1297,15 +1664,17 @@ async function buildCompletionAudit({ cwd, root, slug, state, reviewReworkArtifa
|
|
|
1297
1664
|
const slicesPayload = await readJsonIfExists(state.change_artifact_paths?.slices);
|
|
1298
1665
|
const slices = Array.isArray(slicesPayload?.slices) ? slicesPayload.slices : [];
|
|
1299
1666
|
for (const slice of slices) {
|
|
1667
|
+
const signal = String(slice.verification_signal || '').trim();
|
|
1668
|
+
const usesLegacyGenericSignal = signal === 'execution-record.md verification evidence';
|
|
1669
|
+
const sliceEvidence = usesLegacyGenericSignal
|
|
1670
|
+
? iterationEvidence
|
|
1671
|
+
: iterationEvidence.filter((item) => item.includes(signal));
|
|
1300
1672
|
addChecklistItem({
|
|
1301
1673
|
id: slice.id || `slice-${checklist.length + 1}`,
|
|
1302
1674
|
source: 'vertical-slice',
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
...(iterationData.executionEvidence || []),
|
|
1307
|
-
...(iterationData.verificationEvidence || []),
|
|
1308
|
-
].filter(Boolean),
|
|
1675
|
+
status: sliceEvidence.length > 0 ? 'covered' : 'missing-evidence',
|
|
1676
|
+
requirement: slice.behavior || signal || 'vertical slice',
|
|
1677
|
+
evidence: sliceEvidence,
|
|
1309
1678
|
});
|
|
1310
1679
|
}
|
|
1311
1680
|
|
|
@@ -1362,6 +1731,7 @@ function buildExecutionRecordContent({ slug, iterationData, complete }) {
|
|
|
1362
1731
|
completed_at: nowIso(),
|
|
1363
1732
|
checkpoint_count: iterationData.lanes.length,
|
|
1364
1733
|
evidence_manifest: iterationData.lanes.flatMap((lane) => lane.evidence || []),
|
|
1734
|
+
changed_files: iterationData.changedFiles || [],
|
|
1365
1735
|
}),
|
|
1366
1736
|
`# loopx Execution Record: ${slug}`,
|
|
1367
1737
|
'',
|
|
@@ -2140,8 +2510,10 @@ async function refreshExecutionStatus(root, state) {
|
|
|
2140
2510
|
|
|
2141
2511
|
export async function initWorkspace(cwd, { slug } = {}) {
|
|
2142
2512
|
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
2513
|
+
const projectConventions = await inspectProjectConventions(cwd);
|
|
2143
2514
|
await ensureLoopxRoot(cwd);
|
|
2144
2515
|
await ensureDir(join(workspaceRoot, 'context'));
|
|
2516
|
+
await ensureDir(join(workspaceRoot, 'intake'));
|
|
2145
2517
|
await ensureDir(join(workspaceRoot, 'workflows'));
|
|
2146
2518
|
await ensureDir(join(workspaceRoot, 'specs'));
|
|
2147
2519
|
await ensureDir(join(workspaceRoot, 'changes'));
|
|
@@ -2157,6 +2529,12 @@ export async function initWorkspace(cwd, { slug } = {}) {
|
|
|
2157
2529
|
product_contract: 'skill-first-v1',
|
|
2158
2530
|
default_flow: ['clarify', 'plan', 'build', 'review', 'done', 'archive'],
|
|
2159
2531
|
preferred_surface: ['clarify', 'plan', 'build', 'review', 'archive', 'autopilot'],
|
|
2532
|
+
source_of_truth_policy: projectConventions.source_of_truth_policy,
|
|
2533
|
+
project_conventions: {
|
|
2534
|
+
existing_ai_rules: projectConventions.existing_ai_rules,
|
|
2535
|
+
existing_spec_sources: projectConventions.existing_spec_sources,
|
|
2536
|
+
},
|
|
2537
|
+
verification_commands: projectConventions.verification_commands,
|
|
2160
2538
|
};
|
|
2161
2539
|
|
|
2162
2540
|
if (!existsSync(workspaceConfigPath(workspaceRoot))) {
|
|
@@ -2497,13 +2875,17 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
2497
2875
|
const resumesConsumedReviewPlan = state.current_stage === STAGES.PLAN
|
|
2498
2876
|
&& state.last_confirmed_transition === TRANSITIONS.REVIEW_TO_PLAN
|
|
2499
2877
|
&& state.approval.rollback === APPROVAL_STATES.APPROVED;
|
|
2878
|
+
const resumesClarifyPlan = state.current_stage === STAGES.PLAN
|
|
2879
|
+
&& state.stage_status === 'blocked'
|
|
2880
|
+
&& state.last_confirmed_transition === TRANSITIONS.CLARIFY_TO_PLAN
|
|
2881
|
+
&& state.approval.plan === APPROVAL_STATES.APPROVED;
|
|
2500
2882
|
if (!options.directSpecPath) {
|
|
2501
|
-
if (consumesReviewPlan || resumesConsumedReviewPlan) {
|
|
2502
|
-
// A no-go review may route back to plan; the printed Next command is $plan.
|
|
2883
|
+
if (consumesReviewPlan || resumesConsumedReviewPlan || resumesClarifyPlan) {
|
|
2884
|
+
// A no-go review or a blocked planning run may route back to plan; the printed Next command is $plan.
|
|
2503
2885
|
} else {
|
|
2504
2886
|
ensureApprovedTransition(state, TRANSITIONS.CLARIFY_TO_PLAN, 'plan');
|
|
2505
2887
|
}
|
|
2506
|
-
if (!consumesReviewPlan && !resumesConsumedReviewPlan && state.spec_artifact_path) {
|
|
2888
|
+
if (!consumesReviewPlan && !resumesConsumedReviewPlan && !resumesClarifyPlan && state.spec_artifact_path) {
|
|
2507
2889
|
await copyArtifact(root, state.spec_artifact_path, 'spec.md');
|
|
2508
2890
|
}
|
|
2509
2891
|
}
|
|
@@ -2516,6 +2898,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
2516
2898
|
let architectReview = null;
|
|
2517
2899
|
let criticReview = null;
|
|
2518
2900
|
const reviewArtifactPaths = [];
|
|
2901
|
+
const reviewHistory = initialPlanReviewHistory(state);
|
|
2519
2902
|
|
|
2520
2903
|
while (iteration <= maxIterations) {
|
|
2521
2904
|
const plannerDraft = await adapter.planner({
|
|
@@ -2524,6 +2907,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
2524
2907
|
slug: normalized,
|
|
2525
2908
|
sourceText,
|
|
2526
2909
|
iteration,
|
|
2910
|
+
reviewHistory: [...reviewHistory],
|
|
2527
2911
|
deliberateMode: Boolean(options.deliberate),
|
|
2528
2912
|
interactiveMode: Boolean(options.interactive),
|
|
2529
2913
|
});
|
|
@@ -2554,6 +2938,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
2554
2938
|
});
|
|
2555
2939
|
const reviewPaths = await writePlanReviewArtifacts(root, iteration, plannerDraft, architectReview, criticReview);
|
|
2556
2940
|
reviewArtifactPaths.push(reviewPaths);
|
|
2941
|
+
reviewHistory.push(planReviewSummary(iteration, architectReview, criticReview));
|
|
2557
2942
|
|
|
2558
2943
|
state = {
|
|
2559
2944
|
...state,
|
|
@@ -2573,6 +2958,7 @@ export async function planStage(cwd, slug, options = {}) {
|
|
|
2573
2958
|
plan_package_status: 'complete',
|
|
2574
2959
|
plan_docs_artifact_paths: null,
|
|
2575
2960
|
plan_review_artifact_paths: reviewArtifactPaths,
|
|
2961
|
+
plan_review_history: reviewHistory,
|
|
2576
2962
|
plan_artifact_path: artifactPaths.planPath,
|
|
2577
2963
|
test_spec_artifact_path: artifactPaths.testSpecPath,
|
|
2578
2964
|
change_id: normalizeSlug(changeId),
|
|
@@ -2662,6 +3048,7 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
2662
3048
|
const ownerId = buildOwnerId(normalized);
|
|
2663
3049
|
let iteration = 1;
|
|
2664
3050
|
let current = null;
|
|
3051
|
+
let accumulatedChangedFiles = [];
|
|
2665
3052
|
let blockers = ['build_not_started'];
|
|
2666
3053
|
let delegationLedger = null;
|
|
2667
3054
|
let completionAudit = null;
|
|
@@ -2736,6 +3123,14 @@ export async function buildStage(cwd, slug, options = {}) {
|
|
|
2736
3123
|
contextManifestRows: buildManifest.rows,
|
|
2737
3124
|
contextManifestStatus,
|
|
2738
3125
|
});
|
|
3126
|
+
accumulatedChangedFiles = dedupeStrings([
|
|
3127
|
+
...accumulatedChangedFiles,
|
|
3128
|
+
...(Array.isArray(current.changedFiles) ? current.changedFiles : []),
|
|
3129
|
+
]);
|
|
3130
|
+
current = {
|
|
3131
|
+
...current,
|
|
3132
|
+
changedFiles: accumulatedChangedFiles,
|
|
3133
|
+
};
|
|
2739
3134
|
const supportPaths = resolveBuildSupportPaths(root, current.iteration);
|
|
2740
3135
|
delegationLedgerPath = supportPaths.delegationLedger;
|
|
2741
3136
|
completionAuditPath = supportPaths.completionAudit;
|
|
@@ -2903,14 +3298,14 @@ function reviewFindings({ executionMeta, executionStatus, reviewer, codeReview,
|
|
|
2903
3298
|
if (executionStatus !== 'complete') {
|
|
2904
3299
|
findings.push('execution-record.md 缺少必要的执行或验证证据。');
|
|
2905
3300
|
verdict = 'REQUEST CHANGES';
|
|
2906
|
-
rollbackTarget =
|
|
2907
|
-
rollbackRationale = '
|
|
3301
|
+
rollbackTarget = STAGES.BUILD;
|
|
3302
|
+
rollbackRationale = '执行证据不完整,工作流需要回到 build 阶段补齐执行和验证证据后重新 review。';
|
|
2908
3303
|
}
|
|
2909
3304
|
if (!Array.isArray(executionMeta.evidence_manifest) || executionMeta.evidence_manifest.length === 0) {
|
|
2910
3305
|
findings.push('execution-record.md 缺少必需的 evidence_manifest 结构。');
|
|
2911
3306
|
verdict = 'REQUEST CHANGES';
|
|
2912
|
-
rollbackTarget =
|
|
2913
|
-
rollbackRationale = '执行证据结构不完整,review
|
|
3307
|
+
rollbackTarget = STAGES.BUILD;
|
|
3308
|
+
rollbackRationale = '执行证据结构不完整,review 不能接受本次运行,需要回到 build 阶段补齐 evidence_manifest。';
|
|
2914
3309
|
}
|
|
2915
3310
|
if (executionMeta.actor_id === reviewer) {
|
|
2916
3311
|
findings.push('Reviewer 来源与执行者一致,不满足独立审查要求。');
|
|
@@ -3133,6 +3528,12 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
|
|
|
3133
3528
|
ensureApprovedTransition(state, TRANSITIONS.BUILD_TO_REVIEW, 'review');
|
|
3134
3529
|
}
|
|
3135
3530
|
const { state: refreshed, executionSummary } = await refreshExecutionStatus(root, state);
|
|
3531
|
+
const buildOwnedChangedFilesStatus = Object.hasOwn(executionSummary.meta, 'changed_files')
|
|
3532
|
+
? 'present'
|
|
3533
|
+
: 'unavailable';
|
|
3534
|
+
const buildOwnedChangedFiles = buildOwnedChangedFilesStatus === 'present' && Array.isArray(executionSummary.meta.changed_files)
|
|
3535
|
+
? executionSummary.meta.changed_files
|
|
3536
|
+
: [];
|
|
3136
3537
|
const reviewManifest = await readContextManifest(reviewContextManifestPath(root), { cwd });
|
|
3137
3538
|
ensureValidContextManifest(reviewManifest, STAGES.REVIEW);
|
|
3138
3539
|
const reviewAdapter = adapter || createDefaultReviewAdapter();
|
|
@@ -3146,9 +3547,11 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
|
|
|
3146
3547
|
executionRecordPath: artifactPath(root, 'execution-record.md'),
|
|
3147
3548
|
planArtifactPath: refreshed.plan_artifact_path,
|
|
3148
3549
|
testSpecArtifactPath: refreshed.test_spec_artifact_path,
|
|
3550
|
+
buildOwnedChangedFiles,
|
|
3149
3551
|
contextManifestStatus: reviewManifest.status,
|
|
3150
3552
|
contextManifestPath: reviewContextManifestPath(root),
|
|
3151
3553
|
contextManifestRows: reviewManifest.rows,
|
|
3554
|
+
buildOwnedChangedFilesStatus,
|
|
3152
3555
|
});
|
|
3153
3556
|
} catch (error) {
|
|
3154
3557
|
codeReview = codeReviewFailureResult(error);
|
|
@@ -3168,9 +3571,11 @@ export async function reviewStage(cwd, slug, { reviewer = 'independent-reviewer'
|
|
|
3168
3571
|
planArtifactPath: refreshed.plan_artifact_path,
|
|
3169
3572
|
testSpecArtifactPath: refreshed.test_spec_artifact_path,
|
|
3170
3573
|
changeArtifactPaths: refreshed.change_artifact_paths,
|
|
3574
|
+
buildOwnedChangedFiles,
|
|
3171
3575
|
contextManifestStatus: reviewManifest.status,
|
|
3172
3576
|
contextManifestPath: reviewContextManifestPath(root),
|
|
3173
3577
|
contextManifestRows: reviewManifest.rows,
|
|
3578
|
+
buildOwnedChangedFilesStatus,
|
|
3174
3579
|
});
|
|
3175
3580
|
} catch (error) {
|
|
3176
3581
|
architectureReview = architectureReviewFailureResult(error);
|