@hongmaple0820/scale-engine 0.15.1 → 0.17.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.
Files changed (69) hide show
  1. package/dist/agents/LeadershipPresets.d.ts +16 -0
  2. package/dist/agents/LeadershipPresets.js +152 -0
  3. package/dist/agents/LeadershipPresets.js.map +1 -0
  4. package/dist/api/cli.js +774 -8
  5. package/dist/api/cli.js.map +1 -1
  6. package/dist/artifact/types.d.ts +4 -0
  7. package/dist/artifact/types.js.map +1 -1
  8. package/dist/cli/phaseCommands.d.ts +14 -0
  9. package/dist/cli/phaseCommands.js +187 -6
  10. package/dist/cli/phaseCommands.js.map +1 -1
  11. package/dist/cli/vibeCommands.d.ts +20 -0
  12. package/dist/cli/vibeCommands.js +150 -173
  13. package/dist/cli/vibeCommands.js.map +1 -1
  14. package/dist/index.d.ts +9 -0
  15. package/dist/index.js +10 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/prompts/VibeTemplateGallery.d.ts +25 -0
  18. package/dist/prompts/VibeTemplateGallery.js +295 -0
  19. package/dist/prompts/VibeTemplateGallery.js.map +1 -0
  20. package/dist/skills/SkillRepository.d.ts +63 -0
  21. package/dist/skills/SkillRepository.js +365 -0
  22. package/dist/skills/SkillRepository.js.map +1 -0
  23. package/dist/tools/ToolCapabilityRegistry.d.ts +46 -0
  24. package/dist/tools/ToolCapabilityRegistry.js +223 -0
  25. package/dist/tools/ToolCapabilityRegistry.js.map +1 -0
  26. package/dist/tools/ToolEvidenceGate.d.ts +39 -0
  27. package/dist/tools/ToolEvidenceGate.js +117 -0
  28. package/dist/tools/ToolEvidenceGate.js.map +1 -0
  29. package/dist/tools/ToolEvidenceStore.d.ts +58 -0
  30. package/dist/tools/ToolEvidenceStore.js +129 -0
  31. package/dist/tools/ToolEvidenceStore.js.map +1 -0
  32. package/dist/tools/ToolOrchestrator.d.ts +67 -0
  33. package/dist/tools/ToolOrchestrator.js +246 -0
  34. package/dist/tools/ToolOrchestrator.js.map +1 -0
  35. package/dist/tools/ToolPolicy.d.ts +33 -0
  36. package/dist/tools/ToolPolicy.js +157 -0
  37. package/dist/tools/ToolPolicy.js.map +1 -0
  38. package/dist/tools/index.d.ts +5 -0
  39. package/dist/tools/index.js +6 -0
  40. package/dist/tools/index.js.map +1 -0
  41. package/dist/workflow/ContextGovernance.d.ts +51 -0
  42. package/dist/workflow/ContextGovernance.js +233 -0
  43. package/dist/workflow/ContextGovernance.js.map +1 -0
  44. package/dist/workflow/DiagnosticLoop.d.ts +40 -0
  45. package/dist/workflow/DiagnosticLoop.js +105 -0
  46. package/dist/workflow/DiagnosticLoop.js.map +1 -0
  47. package/dist/workflow/EngineeringStandards.d.ts +69 -0
  48. package/dist/workflow/EngineeringStandards.js +348 -6
  49. package/dist/workflow/EngineeringStandards.js.map +1 -1
  50. package/dist/workflow/GovernanceTemplatePacks.js +11 -9
  51. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
  52. package/dist/workflow/GovernanceTemplates.js +15 -4
  53. package/dist/workflow/GovernanceTemplates.js.map +1 -1
  54. package/dist/workflow/TaskArtifactScaffolder.d.ts +22 -0
  55. package/dist/workflow/TaskArtifactScaffolder.js +55 -0
  56. package/dist/workflow/TaskArtifactScaffolder.js.map +1 -1
  57. package/dist/workflow/TddLoop.d.ts +47 -0
  58. package/dist/workflow/TddLoop.js +76 -0
  59. package/dist/workflow/TddLoop.js.map +1 -0
  60. package/dist/workflow/WorkflowGuidance.d.ts +26 -0
  61. package/dist/workflow/WorkflowGuidance.js +173 -0
  62. package/dist/workflow/WorkflowGuidance.js.map +1 -0
  63. package/dist/workflow/WorkflowOpenTasks.d.ts +16 -0
  64. package/dist/workflow/WorkflowOpenTasks.js +37 -0
  65. package/dist/workflow/WorkflowOpenTasks.js.map +1 -0
  66. package/dist/workflow/index.d.ts +5 -0
  67. package/dist/workflow/index.js +5 -0
  68. package/dist/workflow/index.js.map +1 -1
  69. package/package.json +2 -2
@@ -20,8 +20,15 @@ import { EvidenceStore } from '../workflow/EvidenceStore.js';
20
20
  import { ReviewStore } from '../workflow/ReviewStore.js';
21
21
  import { TaskMetricsStore } from '../workflow/TaskMetricsStore.js';
22
22
  import { appendVerificationArtifact, checkTaskArtifactCompleteness, scaffoldTaskArtifacts } from '../workflow/TaskArtifactScaffolder.js';
23
+ import { createWorkflowGuidance, renderWorkflowGuidance } from '../workflow/WorkflowGuidance.js';
24
+ import { blockingWorkflowOpenTasks, removeWorkflowOpenTask } from '../workflow/WorkflowOpenTasks.js';
23
25
  import { doctorEngineeringStandards, settleEngineeringStandards } from '../workflow/EngineeringStandards.js';
24
26
  import { analyzeReview, parseChangedFiles, shouldReviewFile, summarizeFindings } from '../workflow/ReviewAnalyzer.js';
27
+ import { inspectWorkspaceLifecycle } from '../workflow/WorkspaceLifecycle.js';
28
+ import { evaluateToolEvidenceGate } from '../tools/ToolEvidenceGate.js';
29
+ import { ToolEvidenceStore } from '../tools/ToolEvidenceStore.js';
30
+ import { ToolOrchestrator } from '../tools/ToolOrchestrator.js';
31
+ import { loadToolPolicy } from '../tools/ToolPolicy.js';
25
32
  import { join } from 'node:path';
26
33
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
27
34
  import { HTMLDocumentRenderer } from '../output/HTMLDocumentRenderer.js';
@@ -226,6 +233,49 @@ function evaluateEngineeringStandardsGate(options) {
226
233
  function normalizeEngineeringStandardsGateMode(value) {
227
234
  return value === 'off' || value === 'block' ? value : 'warn';
228
235
  }
236
+ function normalizeToolGateMode(value) {
237
+ if (value === undefined || value === null || value === '')
238
+ return undefined;
239
+ const normalized = String(value).trim().toLowerCase();
240
+ if (normalized === 'warn')
241
+ return 'advisory';
242
+ if (normalized === 'off' || normalized === 'advisory' || normalized === 'evidence-required' || normalized === 'block')
243
+ return normalized;
244
+ throw new Error(`Invalid tool gate mode "${String(value)}"; expected off, advisory, evidence-required, or block.`);
245
+ }
246
+ function resolvePhaseToolGateMode(options) {
247
+ if (isTruthyFlag(options.requireEvidence))
248
+ return 'evidence-required';
249
+ const cliMode = normalizeToolGateMode(options.cliMode);
250
+ if (cliMode)
251
+ return cliMode;
252
+ return options.policy.mode === 'block' ? 'block' : 'off';
253
+ }
254
+ function evaluateTaskToolEvidenceGate(options) {
255
+ if (!options.level || !options.skillPlan)
256
+ return undefined;
257
+ const policy = loadToolPolicy(PROJECT_DIR, SCALE_DIR);
258
+ const mode = resolvePhaseToolGateMode({
259
+ cliMode: options.cliMode,
260
+ requireEvidence: options.requireEvidence,
261
+ policy,
262
+ });
263
+ const effectivePolicy = { ...policy, mode };
264
+ const evidenceStore = new ToolEvidenceStore({ projectDir: PROJECT_DIR, scaleDir: SCALE_DIR });
265
+ const plan = new ToolOrchestrator({
266
+ projectDir: PROJECT_DIR,
267
+ policy: effectivePolicy,
268
+ evidenceStore,
269
+ }).plan({ skillPlan: options.skillPlan });
270
+ return evaluateToolEvidenceGate({
271
+ projectDir: PROJECT_DIR,
272
+ level: options.level,
273
+ plan,
274
+ evidenceStore,
275
+ mode,
276
+ allowSkipped: isTruthyFlag(options.allowSkipped),
277
+ });
278
+ }
229
279
  async function countChangedFiles(taskPayload) {
230
280
  const filesInvolved = taskPayload.filesInvolved ?? [];
231
281
  if (filesInvolved.length > 0)
@@ -729,6 +779,15 @@ export const phaseBuild = defineCommand({
729
779
  skillPlan,
730
780
  });
731
781
  }
782
+ const workflowGuidance = createWorkflowGuidance({
783
+ taskId: task.id,
784
+ description: taskPayloadWithSkills.description,
785
+ level: workflowLevel,
786
+ artifactDir: taskArtifacts?.relativeDir,
787
+ files: taskPayloadWithSkills.filesInvolved,
788
+ skillIntents: taskPayloadWithSkills.skillIntents,
789
+ requiredSkillVerification: taskPayloadWithSkills.requiredSkillVerification,
790
+ });
732
791
  new WorkflowArtifactWriter(SCALE_DIR).updateCurrentState({
733
792
  taskId: task.id,
734
793
  level: workflowLevel,
@@ -743,6 +802,7 @@ export const phaseBuild = defineCommand({
743
802
  recommendedSkills: skillPlan.recommendedSkills,
744
803
  requiredSkillArtifacts: skillPlan.requiredArtifacts,
745
804
  requiredSkillVerification: skillPlan.requiredVerification,
805
+ openTasks: workflowGuidance.items.filter(item => item.required).map(item => item.command),
746
806
  });
747
807
  // FSM transitions: PENDING -> READY -> RUNNING
748
808
  // Phase 1: schedule (PENDING -> READY) - no guards
@@ -765,7 +825,7 @@ export const phaseBuild = defineCommand({
765
825
  if (implResult.allowed) {
766
826
  await fsm.transition(args['plan-id'], 'implement', { actor: { kind: 'system', component: 'phase-build' } });
767
827
  }
768
- const result = { phase: 'BUILD', task: { ...task, payload: taskPayloadWithSkills }, status: 'RUNNING', artifactDir: taskArtifacts?.relativeDir, artifactFiles: taskArtifacts?.created ?? [], skillPlan };
828
+ const result = { phase: 'BUILD', task: { ...task, payload: taskPayloadWithSkills }, status: 'RUNNING', artifactDir: taskArtifacts?.relativeDir, artifactFiles: taskArtifacts?.created ?? [], skillPlan, workflowGuidance };
769
829
  if (args.json)
770
830
  console.log(JSON.stringify(result, null, 2));
771
831
  else {
@@ -780,7 +840,8 @@ export const phaseBuild = defineCommand({
780
840
  console.log(` Recommended skills: ${skillPlan.recommendedSkills.join(', ')}`);
781
841
  if (taskArtifacts?.relativeDir)
782
842
  console.log(` Artifacts: ${taskArtifacts.relativeDir}`);
783
- console.log(`\n Implement now, then run: scale verify ${task.id}\n`);
843
+ console.log(`\n${renderWorkflowGuidance(workflowGuidance)}`);
844
+ console.log('');
784
845
  }
785
846
  },
786
847
  });
@@ -809,6 +870,9 @@ export const phaseVerify = defineCommand({
809
870
  'artifact-gate': { type: 'string', description: 'Task artifact policy override: off, warn, or block' },
810
871
  'require-artifacts': { type: 'boolean', default: false, description: 'Fail verification when required M/L/CRITICAL artifacts are incomplete' },
811
872
  'require-installed-skills': { type: 'boolean', default: false, description: 'Fail verification when required workflow skills are not installed locally' },
873
+ 'tool-gate': { type: 'string', description: 'Tool evidence policy override: off, advisory, evidence-required, or block' },
874
+ 'require-tool-evidence': { type: 'boolean', default: false, description: 'Fail verification when required tool execution evidence is missing or skipped' },
875
+ 'allow-skipped-tool-evidence': { type: 'boolean', default: false, description: 'Allow skipped/manual fallback tool evidence to satisfy the tool gate' },
812
876
  'tdd-evidence': { type: 'string', description: 'Path to JSON TDD evidence with red/green/refactor/testFirst=true' },
813
877
  'tdd-strict': { type: 'boolean', default: false, description: 'Require TDD evidence before other gates' },
814
878
  'residual-risk': { type: 'string', description: 'Residual risk statement to record in task metrics' },
@@ -945,7 +1009,8 @@ export const phaseVerify = defineCommand({
945
1009
  verifiedAt: Date.now(),
946
1010
  };
947
1011
  await store.update(args['task-id'], { payload: updatedPayload });
948
- const workflowState = new WorkflowArtifactWriter(SCALE_DIR).updateCurrentState({
1012
+ const workflowWriter = new WorkflowArtifactWriter(SCALE_DIR);
1013
+ let workflowState = workflowWriter.updateCurrentState({
949
1014
  taskId: args['task-id'],
950
1015
  phase: 'verify',
951
1016
  lastTaskId: args['task-id'],
@@ -986,8 +1051,17 @@ export const phaseVerify = defineCommand({
986
1051
  const requireInstalledSkills = isTruthyFlag(args['require-installed-skills']);
987
1052
  const skillInstallation = inspectRequiredWorkflowSkills(updatedPayload.requiredSkills ?? [], { projectDir: PROJECT_DIR });
988
1053
  const skillInstallationBlocked = requireInstalledSkills && !skillInstallation.ok;
1054
+ const toolEvidenceGate = evaluateTaskToolEvidenceGate({
1055
+ skillPlan: verificationSkillPlan,
1056
+ level: metricLevel,
1057
+ cliMode: args['tool-gate'],
1058
+ requireEvidence: args['require-tool-evidence'],
1059
+ allowSkipped: args['allow-skipped-tool-evidence'],
1060
+ });
1061
+ const workflowOpenTaskBlockers = blockingWorkflowOpenTasks(workflowState.openTasks, args['task-id']);
1062
+ const workflowOpenTasksBlocked = workflowOpenTaskBlockers.length > 0;
989
1063
  // Attempt FSM transition to COMPLETED
990
- // Guards: build_passed, lint_passed, tests_passed, and optional artifact policy.
1064
+ // Guards: build_passed, lint_passed, tests_passed, open workflow tasks, and optional artifact policy.
991
1065
  const codePassed = results.buildStatus === 'success' &&
992
1066
  (results.buildExitCode ?? 1) === 0 &&
993
1067
  results.lintStatus === 'success' &&
@@ -998,7 +1072,9 @@ export const phaseVerify = defineCommand({
998
1072
  !artifactGate.blocked &&
999
1073
  !(skillGate?.blocked ?? false) &&
1000
1074
  !skillInstallationBlocked &&
1001
- !engineeringStandards.blocked;
1075
+ !engineeringStandards.blocked &&
1076
+ !(toolEvidenceGate?.blocked ?? false) &&
1077
+ !workflowOpenTasksBlocked;
1002
1078
  let transitionResult = null;
1003
1079
  if (completionEligible) {
1004
1080
  const completeResult = await fsm.canTransition(args['task-id'], 'complete');
@@ -1033,7 +1109,18 @@ export const phaseVerify = defineCommand({
1033
1109
  else if (!args.json && engineeringStandards.blocked) {
1034
1110
  console.log('\n Engineering standards gate blocked completion - fix blocking standards findings');
1035
1111
  }
1112
+ else if (!args.json && toolEvidenceGate?.blocked) {
1113
+ console.log('\n Tool evidence gate blocked completion - required tools need passed execution evidence');
1114
+ }
1115
+ else if (!args.json && workflowOpenTasksBlocked) {
1116
+ console.log('\n Workflow open tasks blocked completion - finish required workflow commands first');
1117
+ }
1036
1118
  const passed = completionEligible && (transitionResult?.success ?? false);
1119
+ if (passed) {
1120
+ workflowState = workflowWriter.updateCurrentState({
1121
+ openTasks: removeWorkflowOpenTask(workflowState.openTasks, 'verification'),
1122
+ });
1123
+ }
1037
1124
  const verificationArtifactPath = appendVerificationArtifact({
1038
1125
  projectDir: PROJECT_DIR,
1039
1126
  artifactsDir: workflowState.artifactsDir,
@@ -1062,16 +1149,27 @@ export const phaseVerify = defineCommand({
1062
1149
  enforceLevels: skillPolicy.policy.enforceLevels,
1063
1150
  })
1064
1151
  : skillGate;
1152
+ const finalToolEvidenceGate = evaluateTaskToolEvidenceGate({
1153
+ skillPlan: verificationSkillPlan,
1154
+ level: metricLevel,
1155
+ cliMode: args['tool-gate'],
1156
+ requireEvidence: args['require-tool-evidence'],
1157
+ allowSkipped: args['allow-skipped-tool-evidence'],
1158
+ }) ?? toolEvidenceGate;
1065
1159
  const finalPayload = {
1066
1160
  ...updatedPayload,
1067
1161
  artifactGateMode: finalArtifactGate.mode,
1068
1162
  artifactGatePassed: !finalArtifactGate.blocked,
1069
1163
  artifactComplete: artifactCheck?.complete,
1070
1164
  skillGatePassed: finalSkillGate ? !finalSkillGate.blocked && !skillInstallationBlocked : !skillInstallationBlocked,
1165
+ toolOrchestrationMode: finalToolEvidenceGate?.mode,
1166
+ requiredTools: finalToolEvidenceGate?.requiredTools,
1167
+ toolEvidenceIds: finalToolEvidenceGate?.passed.map(item => item.evidenceId).filter((id) => Boolean(id)),
1168
+ toolEvidenceGatePassed: finalToolEvidenceGate ? !finalToolEvidenceGate.blocked : true,
1071
1169
  };
1072
1170
  await store.update(args['task-id'], { payload: finalPayload });
1073
1171
  const metricGateStatus = codePassed &&
1074
- (finalArtifactGate.blocked || finalSkillGate?.blocked || skillInstallationBlocked || engineeringStandards.blocked)
1172
+ (finalArtifactGate.blocked || finalSkillGate?.blocked || skillInstallationBlocked || engineeringStandards.blocked || finalToolEvidenceGate?.blocked || workflowOpenTasksBlocked)
1075
1173
  ? 'blocked'
1076
1174
  : undefined;
1077
1175
  const metricRecord = await recordVerificationMetric({
@@ -1096,6 +1194,12 @@ export const phaseVerify = defineCommand({
1096
1194
  artifactGate: finalArtifactGate,
1097
1195
  engineeringStandards,
1098
1196
  skillGate: finalSkillGate,
1197
+ toolEvidenceGate: finalToolEvidenceGate,
1198
+ workflowOpenTasks: {
1199
+ blocked: workflowOpenTasksBlocked,
1200
+ blockers: workflowOpenTaskBlockers,
1201
+ openTasks: workflowState.openTasks ?? [],
1202
+ },
1099
1203
  skillInstallation: {
1100
1204
  ...skillInstallation,
1101
1205
  checked: requireInstalledSkills,
@@ -1122,6 +1226,9 @@ export const phaseVerify = defineCommand({
1122
1226
  if (engineeringStandards.blocked) {
1123
1227
  console.log(` Engineering standards blockers: ${engineeringStandards.findings.filter(finding => finding.severity === 'fail').length}`);
1124
1228
  }
1229
+ if (finalToolEvidenceGate?.blocked) {
1230
+ console.log(` Tool evidence gaps: ${finalToolEvidenceGate.missing.length} missing, ${finalToolEvidenceGate.failed.length} failed, ${finalToolEvidenceGate.skipped.length} skipped`);
1231
+ }
1125
1232
  if (passed)
1126
1233
  console.log(`\n Next: scale review\n`);
1127
1234
  else
@@ -1252,6 +1359,57 @@ async function stageReviewedFiles(reviewRecords) {
1252
1359
  }
1253
1360
  return { stagedFiles, unreviewedFiles: [] };
1254
1361
  }
1362
+ function isMultiRepositoryTopology(topology) {
1363
+ return topology === 'moe' || topology === 'submodule-workspace' || topology === 'polyrepo';
1364
+ }
1365
+ function hasChildRepositoryChange(path, childPath) {
1366
+ const normalized = normalizeGitPath(path);
1367
+ const child = normalizeGitPath(childPath).replace(/\/+$/, '');
1368
+ return normalized === child || normalized.startsWith(`${child}/`);
1369
+ }
1370
+ async function validateWorkspaceShipBoundary() {
1371
+ try {
1372
+ const report = await inspectWorkspaceLifecycle({ projectDir: PROJECT_DIR });
1373
+ const blockers = [];
1374
+ const warnings = [...report.topology.warnings];
1375
+ const policy = report.topology.finishPolicy;
1376
+ const rootChanges = await getReviewableGitChanges();
1377
+ if (!report.topology.configured && report.childRepositories.length > 0) {
1378
+ const changedChildRepositories = report.childRepositories
1379
+ .filter(child => rootChanges.some(file => hasChildRepositoryChange(file.path, child.relativePath)))
1380
+ .map(child => child.relativePath);
1381
+ const dirtyChildRepositories = report.childRepositories
1382
+ .filter(child => !child.clean || child.ahead > 0)
1383
+ .map(child => child.relativePath);
1384
+ const affected = Array.from(new Set([...changedChildRepositories, ...dirtyChildRepositories]));
1385
+ if (affected.length > 0) {
1386
+ blockers.push(`Workspace topology is not configured; child repository state is present at ${affected.join(', ')}. Create .scale/workspace.json with scale workspace map --write --topology moe before shipping.`);
1387
+ }
1388
+ }
1389
+ if (report.topology.configured && isMultiRepositoryTopology(report.topology.topology)) {
1390
+ for (const child of report.childRepositories) {
1391
+ if (policy.requireCleanRepositories && !child.clean) {
1392
+ blockers.push(`Child repository ${child.relativePath} has uncommitted changes`);
1393
+ }
1394
+ if (policy.requirePushedBranches && child.upstream && child.ahead > 0) {
1395
+ blockers.push(`Child repository ${child.relativePath} has unpushed commits`);
1396
+ }
1397
+ if (policy.requirePushedBranches && report.topology.topology === 'moe' && !child.upstream && child.branch) {
1398
+ blockers.push(`Child repository ${child.relativePath} has no upstream; push or explicitly disable requirePushedBranches before shipping`);
1399
+ }
1400
+ }
1401
+ }
1402
+ return { report, blockers, warnings };
1403
+ }
1404
+ catch (error) {
1405
+ const message = error instanceof Error ? error.message : String(error);
1406
+ return {
1407
+ report: null,
1408
+ blockers: [`Workspace boundary check could not inspect Git repositories: ${message}`],
1409
+ warnings: [],
1410
+ };
1411
+ }
1412
+ }
1255
1413
  function collectTaskReviewText(taskPayload) {
1256
1414
  if (!taskPayload)
1257
1415
  return '';
@@ -1508,6 +1666,7 @@ export const phaseShip = defineCommand({
1508
1666
  const reviewPassed = payload.reviewPassed === true && reviewValidation.ok;
1509
1667
  const artifactGatePassed = payload.artifactGateMode !== 'block' || payload.artifactGatePassed !== false;
1510
1668
  const skillGatePassed = payload.skillGatePassed !== false;
1669
+ const toolEvidenceGatePassed = payload.toolEvidenceGatePassed !== false;
1511
1670
  if (!artifactGatePassed) {
1512
1671
  console.error('\nTask artifact gate did not pass. Complete required task artifacts and re-run: scale verify ' + args['task-id'] + ' --artifact-gate block\n');
1513
1672
  if (payload.artifactComplete === false) {
@@ -1519,6 +1678,13 @@ export const phaseShip = defineCommand({
1519
1678
  console.error('\nTask skill gate did not pass. Complete required skill evidence artifacts and re-run: scale verify ' + args['task-id'] + '\n');
1520
1679
  process.exit(1);
1521
1680
  }
1681
+ if (!toolEvidenceGatePassed) {
1682
+ console.error('\nTask tool evidence gate did not pass. Run required tools and re-run: scale verify ' + args['task-id'] + ' --tool-gate ' + (payload.toolOrchestrationMode ?? 'evidence-required') + '\n');
1683
+ if (payload.requiredTools?.length) {
1684
+ console.error('Required tools: ' + payload.requiredTools.join(', '));
1685
+ }
1686
+ process.exit(1);
1687
+ }
1522
1688
  if (task.status !== 'COMPLETED') {
1523
1689
  if (!verificationPassed) {
1524
1690
  console.error('\nTask not verified with persisted evidence. Run: scale verify ' + args['task-id'] + '\n');
@@ -1555,9 +1721,17 @@ export const phaseShip = defineCommand({
1555
1721
  // Git operations
1556
1722
  let commitHash = null;
1557
1723
  let stagedFiles = [];
1724
+ let workspaceBoundary = null;
1558
1725
  if (!shouldSkipCommit(args['skip-commit'])) {
1559
1726
  const commitMessage = args.message ?? `feat: ${task.title ?? args['task-id']}`;
1560
1727
  try {
1728
+ workspaceBoundary = await validateWorkspaceShipBoundary();
1729
+ if (workspaceBoundary.blockers.length > 0) {
1730
+ console.error('\nWorkspace boundary check failed. Resolve child repositories before shipping the root commit.');
1731
+ workspaceBoundary.blockers.forEach(blocker => console.error(' - ' + blocker));
1732
+ console.error('\nRun scale workspace finish --summary for the shortest fix list, or --json for the full workspace state.\n');
1733
+ process.exit(1);
1734
+ }
1561
1735
  const reviewRecords = getValidatedReviewRecords(payload.reviewEvidenceIds);
1562
1736
  const stageResult = await stageReviewedFiles(reviewRecords);
1563
1737
  if (stageResult.unreviewedFiles.length > 0) {
@@ -1645,6 +1819,13 @@ export const phaseShip = defineCommand({
1645
1819
  reviewValidation,
1646
1820
  commitHash,
1647
1821
  stagedFiles,
1822
+ workspaceBoundary: workspaceBoundary ? {
1823
+ topology: workspaceBoundary.report?.topology.topology ?? null,
1824
+ configured: workspaceBoundary.report?.topology.configured ?? false,
1825
+ childRepositories: workspaceBoundary.report?.childRepositories.length ?? 0,
1826
+ blockers: workspaceBoundary.blockers,
1827
+ warnings: workspaceBoundary.warnings,
1828
+ } : null,
1648
1829
  };
1649
1830
  if (args.json)
1650
1831
  console.log(JSON.stringify(result, null, 2));