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