trackops 2.0.5 → 2.1.0
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 +319 -695
- package/bin/trackops.js +52 -23
- package/lib/cli-format.js +118 -0
- package/lib/config.js +277 -44
- package/lib/control.js +1052 -352
- package/lib/env.js +40 -28
- package/lib/i18n.js +5 -4
- package/lib/init.js +194 -56
- package/lib/opera-bootstrap.js +326 -106
- package/lib/opera-phase-dod.js +485 -0
- package/lib/opera.js +243 -78
- package/lib/plans.js +1329 -0
- package/lib/quality-assert.js +49 -0
- package/lib/quality.js +1759 -0
- package/lib/release.js +18 -11
- package/lib/server.js +504 -192
- package/lib/skills.js +43 -35
- package/lib/workspace.js +32 -21
- package/locales/en.json +431 -75
- package/locales/es.json +432 -76
- package/package.json +6 -5
- package/scripts/quality-unit-tests.js +130 -0
- package/scripts/smoke-tests.js +438 -96
- package/skills/trackops/skill.json +29 -29
- package/templates/skills/opera-quality-guard/SKILL.md +26 -0
- package/templates/skills/opera-quality-guard/locales/en/SKILL.md +26 -0
- package/templates/skills/opera-skill/SKILL.md +8 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +8 -0
- package/ui/js/api.js +93 -26
- package/ui/js/app.js +13 -7
- package/ui/js/filters.js +49 -29
- package/ui/js/time-tracker.js +41 -28
- package/ui/js/views/board.js +22 -14
- package/ui/js/views/dashboard.js +206 -49
- package/ui/js/views/execution.js +7 -3
- package/ui/js/views/plans.js +284 -0
- package/ui/js/views/scrum.js +25 -13
- package/ui/js/views/sidebar.js +9 -8
- package/ui/js/views/tasks.js +238 -134
package/lib/opera-bootstrap.js
CHANGED
|
@@ -9,6 +9,7 @@ const config = require("./config");
|
|
|
9
9
|
const { t, setLocale } = require("./i18n");
|
|
10
10
|
const { isInteractive } = require("./locale");
|
|
11
11
|
const { resolveLocalizedFile } = require("./resources");
|
|
12
|
+
const fmt = require("./cli-format");
|
|
12
13
|
|
|
13
14
|
const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
|
|
14
15
|
|
|
@@ -372,6 +373,7 @@ function directMissingFields(discovery) {
|
|
|
372
373
|
if (!discovery.payload) missing.push("payload");
|
|
373
374
|
if (!Object.keys(discovery.inputSchema || {}).length) missing.push("inputSchema");
|
|
374
375
|
if (!Object.keys(discovery.outputSchema || {}).length) missing.push("outputSchema");
|
|
376
|
+
if (!discovery.decisionOwnership) missing.push("decisionOwnership");
|
|
375
377
|
return missing;
|
|
376
378
|
}
|
|
377
379
|
|
|
@@ -426,9 +428,44 @@ function buildHandoffPrompt(control, profile) {
|
|
|
426
428
|
`- ${t("handoff.instruction.writeSpec")}`,
|
|
427
429
|
`- ${t("handoff.instruction.writeQuestions")}`,
|
|
428
430
|
`- ${t("handoff.instruction.includeFields")}`,
|
|
431
|
+
`- ${t("handoff.instruction.schemaFormat")}`,
|
|
432
|
+
`- ${t("handoff.instruction.decisionOwnership")}`,
|
|
429
433
|
`- ${t("handoff.instruction.respondInLanguage", { language: languageName })}`,
|
|
434
|
+
`- **${t("handoff.instruction.neverRename")}**`,
|
|
435
|
+
`- **${t("handoff.instruction.closure")}**`,
|
|
430
436
|
];
|
|
431
437
|
|
|
438
|
+
lines.push(
|
|
439
|
+
"",
|
|
440
|
+
`## ${t("handoff.section.intakeSchema")}`,
|
|
441
|
+
"",
|
|
442
|
+
t("handoff.instruction.intakeRequired"),
|
|
443
|
+
"",
|
|
444
|
+
"```json",
|
|
445
|
+
JSON.stringify({
|
|
446
|
+
problemStatement: "string — core problem the project solves",
|
|
447
|
+
targetUser: "string — primary user persona",
|
|
448
|
+
singularDesiredOutcome: "string — single most important outcome",
|
|
449
|
+
sourceOfTruth: "string — authoritative data source",
|
|
450
|
+
payload: "string — what gets delivered (NOT 'deliveryTarget')",
|
|
451
|
+
decisionOwnership: "user | shared | agent",
|
|
452
|
+
versionControl: {
|
|
453
|
+
remote: "boolean — whether the project uses a remote VCS",
|
|
454
|
+
provider: "string — github | gitlab | bitbucket | other",
|
|
455
|
+
developmentBranch: "string — main development branch",
|
|
456
|
+
releaseBranch: "string — publish/release branch",
|
|
457
|
+
},
|
|
458
|
+
deployment: {
|
|
459
|
+
mode: "string — manual | ci | platform",
|
|
460
|
+
target: "string — staging | production | preview | custom",
|
|
461
|
+
smokeCommand: "string — command to validate a releasable/deployed build",
|
|
462
|
+
},
|
|
463
|
+
inputSchema: { "fieldName": "type — at least one key required" },
|
|
464
|
+
outputSchema: { "fieldName": "type — at least one key required" },
|
|
465
|
+
}, null, 2),
|
|
466
|
+
"```",
|
|
467
|
+
);
|
|
468
|
+
|
|
432
469
|
if (profile.discovery?.singularDesiredOutcome) {
|
|
433
470
|
lines.push("", `${t("handoff.label.knownIntention")}: ${profile.discovery.singularDesiredOutcome}`);
|
|
434
471
|
}
|
|
@@ -482,21 +519,34 @@ async function collectBootstrapProfile(root, control, options = {}) {
|
|
|
482
519
|
sourceOfTruth: options.answers?.sourceOfTruth || previous.sourceOfTruth || scan.sourceOfTruthHint || "",
|
|
483
520
|
payload: options.answers?.payload || previous.payload || scan.payloadHint || "",
|
|
484
521
|
behaviorRules: normalizeList(options.answers?.behaviorRules || previous.behaviorRules || ""),
|
|
485
|
-
inputSchema: options.answers?.inputSchema || previous.inputSchema || {},
|
|
486
|
-
outputSchema: options.answers?.outputSchema || previous.outputSchema || {},
|
|
487
|
-
architecturalInvariants: normalizeList(options.answers?.architecturalInvariants || previous.architecturalInvariants || ""),
|
|
488
|
-
pipeline: normalizeList(options.answers?.pipeline || previous.pipeline || ""),
|
|
489
|
-
templates: normalizeList(options.answers?.templates || previous.templates || ""),
|
|
490
|
-
|
|
491
|
-
|
|
522
|
+
inputSchema: options.answers?.inputSchema || previous.inputSchema || {},
|
|
523
|
+
outputSchema: options.answers?.outputSchema || previous.outputSchema || {},
|
|
524
|
+
architecturalInvariants: normalizeList(options.answers?.architecturalInvariants || previous.architecturalInvariants || ""),
|
|
525
|
+
pipeline: normalizeList(options.answers?.pipeline || previous.pipeline || ""),
|
|
526
|
+
templates: normalizeList(options.answers?.templates || previous.templates || ""),
|
|
527
|
+
versionControl: options.answers?.versionControl || previous.versionControl || {
|
|
528
|
+
remote: false,
|
|
529
|
+
provider: "",
|
|
530
|
+
developmentBranch: context.branches.development || "develop",
|
|
531
|
+
releaseBranch: context.branches.publish || "master",
|
|
532
|
+
},
|
|
533
|
+
deployment: options.answers?.deployment || previous.deployment || {
|
|
534
|
+
mode: "",
|
|
535
|
+
target: "",
|
|
536
|
+
smokeCommand: "",
|
|
537
|
+
},
|
|
538
|
+
availableArtifacts: Array.isArray(previousDiscovery.availableArtifacts) ? previousDiscovery.availableArtifacts : [],
|
|
539
|
+
};
|
|
492
540
|
|
|
493
541
|
const answers = { ...defaults };
|
|
494
542
|
|
|
495
543
|
if (interactive) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
544
|
+
fmt.header(t("bootstrap.header"));
|
|
545
|
+
fmt.blank();
|
|
546
|
+
fmt.info(t("bootstrap.subtitle"));
|
|
547
|
+
fmt.blank();
|
|
548
|
+
fmt.info(t("bootstrap.instructions"));
|
|
549
|
+
fmt.blank();
|
|
500
550
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
501
551
|
try {
|
|
502
552
|
answers.technicalLevel = await askEnumQuestion(rl, t("bootstrap.question.technicalLevel"), defaults.technicalLevel || "medium", TECHNICAL_LEVELS) || "medium";
|
|
@@ -593,25 +643,128 @@ function parseSpecSections(specText) {
|
|
|
593
643
|
);
|
|
594
644
|
}
|
|
595
645
|
|
|
596
|
-
function buildOpenQuestions(missingFields, contradictions) {
|
|
597
|
-
const lines = [
|
|
598
|
-
if (missingFields.length) {
|
|
599
|
-
lines.push(
|
|
600
|
-
missingFields.forEach((field) => lines.push(`- ${field}`));
|
|
601
|
-
lines.push("");
|
|
602
|
-
}
|
|
603
|
-
if (contradictions.length) {
|
|
604
|
-
lines.push(
|
|
605
|
-
contradictions.forEach((item) => lines.push(`- ${item}`));
|
|
606
|
-
lines.push("");
|
|
607
|
-
}
|
|
608
|
-
if (!missingFields.length && !contradictions.length) {
|
|
609
|
-
lines.push("
|
|
610
|
-
}
|
|
611
|
-
return `${lines.join("\n")}\n`;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function
|
|
646
|
+
function buildOpenQuestions(missingFields, contradictions) {
|
|
647
|
+
const lines = [`# ${t("bootstrap.scaffold.openQuestionsTitle")}`, ""];
|
|
648
|
+
if (missingFields.length) {
|
|
649
|
+
lines.push(`## ${t("bootstrap.scaffold.missingFields")}`, "");
|
|
650
|
+
missingFields.forEach((field) => lines.push(`- ${field}`));
|
|
651
|
+
lines.push("");
|
|
652
|
+
}
|
|
653
|
+
if (contradictions.length) {
|
|
654
|
+
lines.push(`## ${t("bootstrap.scaffold.contradictions")}`, "");
|
|
655
|
+
contradictions.forEach((item) => lines.push(`- ${item}`));
|
|
656
|
+
lines.push("");
|
|
657
|
+
}
|
|
658
|
+
if (!missingFields.length && !contradictions.length) {
|
|
659
|
+
lines.push(`- ${t("bootstrap.scaffold.none")}`);
|
|
660
|
+
}
|
|
661
|
+
return `${lines.join("\n")}\n`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function buildDirectIntakeTemplate(profile) {
|
|
665
|
+
return {
|
|
666
|
+
version: 1,
|
|
667
|
+
technicalLevel: profile.technicalLevel || null,
|
|
668
|
+
projectState: profile.projectState || null,
|
|
669
|
+
documentationState: profile.documentationState || null,
|
|
670
|
+
decisionOwnership: profile.discovery?.decisionOwnership || profile.decisionOwnership || null,
|
|
671
|
+
problemStatement: profile.discovery?.problemStatement || "",
|
|
672
|
+
targetUser: profile.discovery?.targetUser || "",
|
|
673
|
+
singularDesiredOutcome: profile.discovery?.singularDesiredOutcome || "",
|
|
674
|
+
userLanguage: profile.discovery?.userLanguage || null,
|
|
675
|
+
needsPlainLanguage: Boolean(profile.discovery?.needsPlainLanguage),
|
|
676
|
+
recommendedStack: normalizeList(profile.discovery?.recommendedStack || []),
|
|
677
|
+
externalServices: normalizeList(profile.discovery?.externalServices || []),
|
|
678
|
+
sourceOfTruth: profile.discovery?.sourceOfTruth || "",
|
|
679
|
+
payload: profile.discovery?.payload || "",
|
|
680
|
+
behaviorRules: normalizeList(profile.discovery?.behaviorRules || []),
|
|
681
|
+
inputSchema: profile.discovery?.inputSchema || {},
|
|
682
|
+
outputSchema: profile.discovery?.outputSchema || {},
|
|
683
|
+
architecturalInvariants: normalizeList(profile.discovery?.architecturalInvariants || []),
|
|
684
|
+
pipeline: normalizeList(profile.discovery?.pipeline || []),
|
|
685
|
+
templates: normalizeList(profile.discovery?.templates || []),
|
|
686
|
+
versionControl: profile.discovery?.versionControl || {
|
|
687
|
+
remote: false,
|
|
688
|
+
provider: "",
|
|
689
|
+
developmentBranch: "",
|
|
690
|
+
releaseBranch: "",
|
|
691
|
+
},
|
|
692
|
+
deployment: profile.discovery?.deployment || {
|
|
693
|
+
mode: "",
|
|
694
|
+
target: "",
|
|
695
|
+
smokeCommand: "",
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function buildSpecDossierScaffold(profile) {
|
|
701
|
+
return [
|
|
702
|
+
`# ${t("bootstrap.scaffold.specTitle")}`,
|
|
703
|
+
"",
|
|
704
|
+
`## ${t("bootstrap.scaffold.problemStatement")}`,
|
|
705
|
+
profile.discovery?.problemStatement || "",
|
|
706
|
+
"",
|
|
707
|
+
`## ${t("bootstrap.scaffold.targetUser")}`,
|
|
708
|
+
profile.discovery?.targetUser || "",
|
|
709
|
+
"",
|
|
710
|
+
`## ${t("bootstrap.scaffold.singularDesiredOutcome")}`,
|
|
711
|
+
profile.discovery?.singularDesiredOutcome || "",
|
|
712
|
+
"",
|
|
713
|
+
`## ${t("bootstrap.scaffold.deliveryTarget")}`,
|
|
714
|
+
profile.discovery?.payload || "",
|
|
715
|
+
"",
|
|
716
|
+
`## ${t("bootstrap.scaffold.sourceOfTruth")}`,
|
|
717
|
+
profile.discovery?.sourceOfTruth || "",
|
|
718
|
+
"",
|
|
719
|
+
].join("\n");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function ensureDirectBootstrapArtifacts(context, profile) {
|
|
723
|
+
const files = bootstrapFilePaths(context);
|
|
724
|
+
const existingIntake = readJson(files.intakeJson);
|
|
725
|
+
const intake = existingIntake && typeof existingIntake === "object"
|
|
726
|
+
? existingIntake
|
|
727
|
+
: buildDirectIntakeTemplate(profile);
|
|
728
|
+
if (!existingIntake) {
|
|
729
|
+
writeJson(files.intakeJson, intake);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let specText = readText(files.specDossier);
|
|
733
|
+
if (!specText.trim()) {
|
|
734
|
+
specText = buildSpecDossierScaffold({ ...profile, discovery: { ...profile.discovery, ...intake } });
|
|
735
|
+
writeText(files.specDossier, specText);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const mergedProfile = {
|
|
739
|
+
...profile,
|
|
740
|
+
discovery: {
|
|
741
|
+
...(profile.discovery || {}),
|
|
742
|
+
...intake,
|
|
743
|
+
externalServices: normalizeList(intake.externalServices || profile.discovery?.externalServices || []),
|
|
744
|
+
behaviorRules: normalizeList(intake.behaviorRules || profile.discovery?.behaviorRules || []),
|
|
745
|
+
architecturalInvariants: normalizeList(intake.architecturalInvariants || profile.discovery?.architecturalInvariants || []),
|
|
746
|
+
pipeline: normalizeList(intake.pipeline || profile.discovery?.pipeline || []),
|
|
747
|
+
templates: normalizeList(intake.templates || profile.discovery?.templates || []),
|
|
748
|
+
inputSchema: intake.inputSchema || profile.discovery?.inputSchema || {},
|
|
749
|
+
outputSchema: intake.outputSchema || profile.discovery?.outputSchema || {},
|
|
750
|
+
decisionOwnership: intake.decisionOwnership || profile.discovery?.decisionOwnership || profile.decisionOwnership || null,
|
|
751
|
+
versionControl: intake.versionControl || profile.discovery?.versionControl || null,
|
|
752
|
+
deployment: intake.deployment || profile.discovery?.deployment || null,
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
const qualityReport = buildQualityReport(context, mergedProfile, specText);
|
|
756
|
+
writeText(files.openQuestions, buildOpenQuestions(qualityReport.missingFields, qualityReport.contradictions));
|
|
757
|
+
writeJson(files.qualityReport, qualityReport);
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
intake,
|
|
761
|
+
specText,
|
|
762
|
+
qualityReport,
|
|
763
|
+
profile: mergedProfile,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function qualityStatusFor(missingFields, contradictions) {
|
|
615
768
|
if (missingFields.length >= 2) return "blocked";
|
|
616
769
|
if (missingFields.length || contradictions.length) return "needs_review";
|
|
617
770
|
return "ready";
|
|
@@ -630,23 +783,21 @@ function contractReadinessFor(profile, qualityStatus) {
|
|
|
630
783
|
return "verified";
|
|
631
784
|
}
|
|
632
785
|
|
|
633
|
-
function buildQualityReport(context, profile, specText) {
|
|
634
|
-
const sections = parseSpecSections(specText);
|
|
635
|
-
const missingFields = directMissingFields(profile.discovery);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
["
|
|
643
|
-
["
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
[
|
|
647
|
-
|
|
648
|
-
for (const [sectionName, fieldName, expected] of mappings) {
|
|
649
|
-
const actual = sections[sectionName];
|
|
786
|
+
function buildQualityReport(context, profile, specText) {
|
|
787
|
+
const sections = parseSpecSections(specText);
|
|
788
|
+
const missingFields = directMissingFields(profile.discovery);
|
|
789
|
+
|
|
790
|
+
const contradictions = [];
|
|
791
|
+
const mappings = [
|
|
792
|
+
["problem statement", "problemStatement", profile.discovery.problemStatement, "problema principal"],
|
|
793
|
+
["target user", "targetUser", profile.discovery.targetUser, "usuario objetivo", "usuario objetivo principal"],
|
|
794
|
+
["singular desired outcome", "singularDesiredOutcome", profile.discovery.singularDesiredOutcome, "resultado singular deseado", "resultado deseado"],
|
|
795
|
+
["payload", "payload", profile.discovery.payload, "delivery target", "objetivo de entrega"],
|
|
796
|
+
["source of truth", "sourceOfTruth", profile.discovery.sourceOfTruth, "fuente de la verdad"],
|
|
797
|
+
];
|
|
798
|
+
for (const mapping of mappings) {
|
|
799
|
+
const [sectionName, fieldName, expected, ...aliases] = mapping;
|
|
800
|
+
const actual = sections[sectionName] || aliases.reduce((found, alias) => found || sections[alias], null);
|
|
650
801
|
if (!actual) {
|
|
651
802
|
missingFields.push(fieldName);
|
|
652
803
|
continue;
|
|
@@ -700,23 +851,25 @@ function buildOperatingContract(control, profile, qualityReport, context) {
|
|
|
700
851
|
sourceArtifacts: discovery.availableArtifacts || [],
|
|
701
852
|
repoScan: profile.inference || {},
|
|
702
853
|
},
|
|
703
|
-
system: {
|
|
704
|
-
sourceOfTruth: discovery.sourceOfTruth || "",
|
|
705
|
-
externalServices: discovery.externalServices || [],
|
|
706
|
-
inputSchema: discovery.inputSchema || {},
|
|
707
|
-
outputSchema: discovery.outputSchema || {},
|
|
708
|
-
behaviorRules: discovery.behaviorRules || [],
|
|
709
|
-
architecturalInvariants: discovery.architecturalInvariants || [],
|
|
710
|
-
},
|
|
711
|
-
execution: {
|
|
712
|
-
pipeline: discovery.pipeline || [],
|
|
713
|
-
templates: discovery.templates || [],
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
854
|
+
system: {
|
|
855
|
+
sourceOfTruth: discovery.sourceOfTruth || "",
|
|
856
|
+
externalServices: discovery.externalServices || [],
|
|
857
|
+
inputSchema: discovery.inputSchema || {},
|
|
858
|
+
outputSchema: discovery.outputSchema || {},
|
|
859
|
+
behaviorRules: discovery.behaviorRules || [],
|
|
860
|
+
architecturalInvariants: discovery.architecturalInvariants || [],
|
|
861
|
+
},
|
|
862
|
+
execution: {
|
|
863
|
+
pipeline: discovery.pipeline || [],
|
|
864
|
+
templates: discovery.templates || [],
|
|
865
|
+
deployment: discovery.deployment || null,
|
|
866
|
+
phaseModel: "opera-v3",
|
|
867
|
+
taskSeeds: buildSeedTasks(control, profile).map((task) => task.id),
|
|
868
|
+
},
|
|
869
|
+
governance: {
|
|
870
|
+
policyFile: policyRelativePath(context),
|
|
871
|
+
versionControl: discovery.versionControl || null,
|
|
872
|
+
riskProfile: "standard",
|
|
720
873
|
approvalRules: [
|
|
721
874
|
"destructive_changes_require_approval",
|
|
722
875
|
"production_deploy_requires_approval",
|
|
@@ -795,7 +948,10 @@ function renderGenesis(control, contract) {
|
|
|
795
948
|
}
|
|
796
949
|
|
|
797
950
|
function buildSeedTasks(control, profile) {
|
|
798
|
-
|
|
951
|
+
let phaseOrder = config.getPhases(control);
|
|
952
|
+
if (!phaseOrder || !phaseOrder.length) {
|
|
953
|
+
phaseOrder = config.buildDefaultPhases ? config.buildDefaultPhases(config.getLocale(control)) : config.DEFAULT_PHASES;
|
|
954
|
+
}
|
|
799
955
|
const firstTaskStatus =
|
|
800
956
|
profile.status === "completed"
|
|
801
957
|
? "completed"
|
|
@@ -986,8 +1142,21 @@ function applyBootstrap(root, control, profile) {
|
|
|
986
1142
|
control.meta.phases = config.getPhases(control);
|
|
987
1143
|
control.meta.opera.legacyStatus = "supported";
|
|
988
1144
|
|
|
1145
|
+
const existingBootstrap = new Map(
|
|
1146
|
+
(control.tasks || []).filter((task) => task.origin === "bootstrap").map((task) => [task.id, task])
|
|
1147
|
+
);
|
|
989
1148
|
const remainingTasks = (control.tasks || []).filter((task) => task.id !== "ops-bootstrap" && task.origin !== "bootstrap");
|
|
990
|
-
|
|
1149
|
+
const newSeeds = buildSeedTasks(control, profile).map((seed) => {
|
|
1150
|
+
const existing = existingBootstrap.get(seed.id);
|
|
1151
|
+
if (!existing) return seed;
|
|
1152
|
+
return {
|
|
1153
|
+
...seed,
|
|
1154
|
+
status: existing.status === "completed" ? "completed" : seed.status,
|
|
1155
|
+
history: existing.history || seed.history,
|
|
1156
|
+
blocker: existing.status === "blocked" ? existing.blocker : seed.blocker,
|
|
1157
|
+
};
|
|
1158
|
+
});
|
|
1159
|
+
control.tasks = [...remainingTasks, ...newSeeds];
|
|
991
1160
|
|
|
992
1161
|
control.decisionsPending = (profile.missingFields || [])
|
|
993
1162
|
.filter((field) => !["intakeJson", "specDossier"].includes(field))
|
|
@@ -1005,37 +1174,54 @@ function applyBootstrap(root, control, profile) {
|
|
|
1005
1174
|
}
|
|
1006
1175
|
|
|
1007
1176
|
control.findings = control.findings || [];
|
|
1008
|
-
const files = bootstrapFilePaths(context);
|
|
1009
|
-
if (profile.mode === "agent_handoff") {
|
|
1010
|
-
writeJson(files.json, buildHandoffPayload(control, profile, context));
|
|
1011
|
-
writeText(files.markdown, buildHandoffPrompt(control, profile));
|
|
1012
|
-
if (!fs.existsSync(files.specDossier)) {
|
|
1013
|
-
writeText(
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1177
|
+
const files = bootstrapFilePaths(context);
|
|
1178
|
+
if (profile.mode === "agent_handoff") {
|
|
1179
|
+
writeJson(files.json, buildHandoffPayload(control, profile, context));
|
|
1180
|
+
writeText(files.markdown, buildHandoffPrompt(control, profile));
|
|
1181
|
+
if (!fs.existsSync(files.specDossier)) {
|
|
1182
|
+
writeText(
|
|
1183
|
+
files.specDossier,
|
|
1184
|
+
`# ${t("bootstrap.scaffold.specTitle")}\n\n${t("bootstrap.scaffold.specPlaceholder")}\n`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
writeText(files.openQuestions, buildOpenQuestions(["intakeJson", "specDossier"], []));
|
|
1188
|
+
}
|
|
1189
|
+
let directArtifacts = null;
|
|
1190
|
+
if (profile.mode === "direct_cli") {
|
|
1191
|
+
directArtifacts = ensureDirectBootstrapArtifacts(context, profile);
|
|
1192
|
+
profile = directArtifacts.profile;
|
|
1193
|
+
control.meta.opera.bootstrap = {
|
|
1194
|
+
...control.meta.opera.bootstrap,
|
|
1195
|
+
...profile,
|
|
1196
|
+
handoffFiles: null,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
const genesisPath = context.paths.genesisFile;
|
|
1200
|
+
if (profile.status === "completed") {
|
|
1201
|
+
const specText = directArtifacts?.specText || (
|
|
1202
|
+
fs.existsSync(files.specDossier) && readText(files.specDossier).trim()
|
|
1203
|
+
? readText(files.specDossier)
|
|
1204
|
+
: `## Problem statement\n${profile.discovery.problemStatement || ""}\n\n## Target user\n${profile.discovery.targetUser || ""}\n\n## Singular desired outcome\n${profile.discovery.singularDesiredOutcome || ""}\n\n## Delivery target\n${profile.discovery.payload || ""}\n\n## Source of truth\n${profile.discovery.sourceOfTruth || ""}\n`
|
|
1205
|
+
);
|
|
1206
|
+
const qualityReport = directArtifacts?.qualityReport || profile.qualityReport || buildQualityReport(context, profile, specText);
|
|
1207
|
+
const contract = buildOperatingContract(control, profile, qualityReport, context);
|
|
1208
|
+
writeJson(context.paths.contractFile, contract);
|
|
1209
|
+
fs.writeFileSync(genesisPath, `${renderGenesis(control, contract)}\n`, "utf8");
|
|
1210
|
+
control.meta.opera.contractVersion = CONTRACT_VERSION;
|
|
1027
1211
|
control.meta.opera.contractReadiness = qualityReport.contractReadiness;
|
|
1028
1212
|
control.meta.opera.contractFile = contractRelativePath(context);
|
|
1029
1213
|
control.meta.opera.qualityStatus = qualityReport.status;
|
|
1030
|
-
} else if (["needs_review", "blocked"].includes(profile.status)) {
|
|
1031
|
-
|
|
1032
|
-
control.meta.opera.
|
|
1033
|
-
control.meta.opera.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
control.meta.opera.
|
|
1038
|
-
|
|
1214
|
+
} else if (["needs_review", "blocked"].includes(profile.status)) {
|
|
1215
|
+
const qualityReport = directArtifacts?.qualityReport || profile.qualityReport || null;
|
|
1216
|
+
control.meta.opera.contractVersion = null;
|
|
1217
|
+
control.meta.opera.contractReadiness = qualityReport?.contractReadiness || "hypothesis";
|
|
1218
|
+
control.meta.opera.qualityStatus = qualityReport?.status || profile.status;
|
|
1219
|
+
} else {
|
|
1220
|
+
const qualityReport = directArtifacts?.qualityReport || profile.qualityReport || null;
|
|
1221
|
+
control.meta.opera.contractVersion = null;
|
|
1222
|
+
control.meta.opera.contractReadiness = qualityReport?.contractReadiness || "hypothesis";
|
|
1223
|
+
control.meta.opera.qualityStatus = qualityReport?.status || profile.status;
|
|
1224
|
+
}
|
|
1039
1225
|
|
|
1040
1226
|
config.saveControl(context, control);
|
|
1041
1227
|
return control;
|
|
@@ -1051,9 +1237,14 @@ function resumeBootstrap(root, control) {
|
|
|
1051
1237
|
const files = bootstrapFilePaths(context);
|
|
1052
1238
|
const intake = readJson(files.intakeJson);
|
|
1053
1239
|
const specDossier = readText(files.specDossier);
|
|
1054
|
-
if (!intake ||
|
|
1240
|
+
if (!intake || typeof intake !== "object") {
|
|
1055
1241
|
return { resumed: false, status: "awaiting_agent", reason: "missing_agent_artifacts" };
|
|
1056
1242
|
}
|
|
1243
|
+
const meaningfulKeys = ["problemStatement", "targetUser", "singularDesiredOutcome", "payload", "sourceOfTruth"];
|
|
1244
|
+
const hasContent = meaningfulKeys.some((k) => intake[k] && String(intake[k]).trim());
|
|
1245
|
+
if (!hasContent && !specDossier.trim()) {
|
|
1246
|
+
return { resumed: false, status: "awaiting_agent", reason: "empty_intake_and_spec" };
|
|
1247
|
+
}
|
|
1057
1248
|
|
|
1058
1249
|
const discovery = {
|
|
1059
1250
|
...bootstrap.discovery,
|
|
@@ -1062,11 +1253,13 @@ function resumeBootstrap(root, control) {
|
|
|
1062
1253
|
externalServices: normalizeList(intake.externalServices || bootstrap.discovery?.externalServices || []),
|
|
1063
1254
|
behaviorRules: normalizeList(intake.behaviorRules || bootstrap.discovery?.behaviorRules || []),
|
|
1064
1255
|
architecturalInvariants: normalizeList(intake.architecturalInvariants || bootstrap.discovery?.architecturalInvariants || []),
|
|
1065
|
-
pipeline: normalizeList(intake.pipeline || bootstrap.discovery?.pipeline || []),
|
|
1066
|
-
templates: normalizeList(intake.templates || bootstrap.discovery?.templates || []),
|
|
1067
|
-
inputSchema: intake.inputSchema || bootstrap.discovery?.inputSchema || {},
|
|
1068
|
-
outputSchema: intake.outputSchema || bootstrap.discovery?.outputSchema || {},
|
|
1069
|
-
|
|
1256
|
+
pipeline: normalizeList(intake.pipeline || bootstrap.discovery?.pipeline || []),
|
|
1257
|
+
templates: normalizeList(intake.templates || bootstrap.discovery?.templates || []),
|
|
1258
|
+
inputSchema: intake.inputSchema || bootstrap.discovery?.inputSchema || {},
|
|
1259
|
+
outputSchema: intake.outputSchema || bootstrap.discovery?.outputSchema || {},
|
|
1260
|
+
versionControl: intake.versionControl || bootstrap.discovery?.versionControl || null,
|
|
1261
|
+
deployment: intake.deployment || bootstrap.discovery?.deployment || null,
|
|
1262
|
+
};
|
|
1070
1263
|
const missingFields = directMissingFields(discovery);
|
|
1071
1264
|
const profile = {
|
|
1072
1265
|
...bootstrap,
|
|
@@ -1081,12 +1274,13 @@ function resumeBootstrap(root, control) {
|
|
|
1081
1274
|
discovery,
|
|
1082
1275
|
inference: scanProject(context),
|
|
1083
1276
|
};
|
|
1084
|
-
const qualityReport = buildQualityReport(context, profile, specDossier);
|
|
1085
|
-
profile.status = qualityReport.status === "ready" ? "completed" : qualityReport.status;
|
|
1086
|
-
profile.completedAt = qualityReport.status === "ready" ? nowIso() : null;
|
|
1087
|
-
profile.
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1277
|
+
const qualityReport = buildQualityReport(context, profile, specDossier);
|
|
1278
|
+
profile.status = qualityReport.status === "ready" ? "completed" : qualityReport.status;
|
|
1279
|
+
profile.completedAt = qualityReport.status === "ready" ? nowIso() : null;
|
|
1280
|
+
profile.missingFields = qualityReport.missingFields || [];
|
|
1281
|
+
profile.qualityReport = qualityReport;
|
|
1282
|
+
return { resumed: true, profile };
|
|
1283
|
+
}
|
|
1090
1284
|
|
|
1091
1285
|
function detectLegacyBootstrap(root, control) {
|
|
1092
1286
|
const context = config.ensureContext(root);
|
|
@@ -1115,6 +1309,31 @@ function detectLegacyBootstrap(root, control) {
|
|
|
1115
1309
|
return null;
|
|
1116
1310
|
}
|
|
1117
1311
|
|
|
1312
|
+
function revalidateContract(root, control) {
|
|
1313
|
+
const context = config.ensureContext(root);
|
|
1314
|
+
if (!config.isOperaInstalled(control)) return { changed: false };
|
|
1315
|
+
if (!fs.existsSync(context.paths.contractFile)) return { changed: false };
|
|
1316
|
+
const files = bootstrapFilePaths(context);
|
|
1317
|
+
const intake = readJson(files.intakeJson);
|
|
1318
|
+
const specDossier = readText(files.specDossier);
|
|
1319
|
+
if (!intake) return { changed: false };
|
|
1320
|
+
const bootstrapState = getBootstrapState(control, context);
|
|
1321
|
+
if (!bootstrapState || !bootstrapState.discovery) return { changed: false };
|
|
1322
|
+
const discovery = { ...bootstrapState.discovery, ...intake };
|
|
1323
|
+
const profile = { ...bootstrapState, discovery };
|
|
1324
|
+
const qualityReport = buildQualityReport(context, profile, specDossier);
|
|
1325
|
+
const newContract = buildOperatingContract(control, profile, qualityReport, context);
|
|
1326
|
+
const existing = readJson(context.paths.contractFile);
|
|
1327
|
+
const changed = JSON.stringify(existing) !== JSON.stringify(newContract);
|
|
1328
|
+
if (changed) {
|
|
1329
|
+
writeJson(context.paths.contractFile, newContract);
|
|
1330
|
+
control.meta.opera.contractReadiness = qualityReport.contractReadiness;
|
|
1331
|
+
control.meta.opera.qualityStatus = qualityReport.status === "ready" ? "completed" : qualityReport.status;
|
|
1332
|
+
config.saveControl(context, control);
|
|
1333
|
+
}
|
|
1334
|
+
return { changed, contractReadiness: qualityReport.contractReadiness };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1118
1337
|
module.exports = {
|
|
1119
1338
|
TECHNICAL_LEVELS,
|
|
1120
1339
|
PROJECT_STATES,
|
|
@@ -1132,6 +1351,7 @@ module.exports = {
|
|
|
1132
1351
|
collectBootstrapProfile,
|
|
1133
1352
|
applyBootstrap,
|
|
1134
1353
|
resumeBootstrap,
|
|
1354
|
+
revalidateContract,
|
|
1135
1355
|
detectLegacyBootstrap,
|
|
1136
1356
|
getBootstrapState,
|
|
1137
1357
|
buildQualityReport,
|