@planu/cli 5.0.0 → 5.2.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/CHANGELOG.md +76 -1
- package/dist/cli/commands/doctor.d.ts +22 -0
- package/dist/cli/commands/doctor.js +176 -2
- package/dist/cli/commands/spec.js +10 -1
- package/dist/core/spec-validator.js +32 -18
- package/dist/engine/autopilot/bootstrap.js +27 -0
- package/dist/engine/core-bridge.d.ts +28 -0
- package/dist/engine/core-bridge.js +67 -0
- package/dist/engine/drift-monitor.js +16 -18
- package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
- package/dist/engine/evidence-gates/artifact-reader.js +59 -2
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
- package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
- package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
- package/dist/engine/execution/operation-journal.js +10 -4
- package/dist/engine/living-spec/hash-tracker.js +26 -28
- package/dist/engine/minimality/policy-loader.js +247 -6
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
- package/dist/engine/reverse-engineer/api-detector.js +2 -13
- package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
- package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
- package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
- package/dist/engine/spec-format/acceptance-criteria.js +13 -12
- package/dist/engine/spec-format/text-fences.js +20 -2
- package/dist/engine/spec-language/english-only.d.ts +14 -0
- package/dist/engine/spec-language/english-only.js +58 -0
- package/dist/engine/spec-migrator/criteria-scanner.js +11 -13
- package/dist/engine/spec-migrator/drift-detector.js +10 -12
- package/dist/engine/spec-state-syncer.js +1 -1
- package/dist/engine/timing/budget.js +5 -1
- package/dist/engine/vector-store/tfidf.d.ts +13 -9
- package/dist/engine/vector-store/tfidf.js +26 -0
- package/dist/engine/worker-config-loader.d.ts +1 -1
- package/dist/engine/worker-config-loader.js +1 -11
- package/dist/engine/workers/schema.d.ts +0 -8
- package/dist/engine/workers/schema.js +0 -1
- package/dist/i18n/index.d.ts +18 -0
- package/dist/i18n/index.js +40 -1
- package/dist/server/routes/specs.js +7 -5
- package/dist/storage/global-store.d.ts +9 -0
- package/dist/storage/global-store.js +23 -0
- package/dist/storage/semantic-index-store.d.ts +23 -0
- package/dist/storage/semantic-index-store.js +105 -0
- package/dist/storage/status-store/self-healing.js +15 -17
- package/dist/tools/challenge-spec/scenarios-utils.js +5 -1
- package/dist/tools/challenge-spec-helpers.d.ts +10 -1
- package/dist/tools/challenge-spec-helpers.js +63 -22
- package/dist/tools/challenge-spec.js +18 -3
- package/dist/tools/check-readiness.js +37 -13
- package/dist/tools/create-spec/spec-builder.d.ts +7 -0
- package/dist/tools/create-spec/spec-builder.js +19 -4
- package/dist/tools/create-spec.js +216 -101
- package/dist/tools/init-project/handler.js +78 -25
- package/dist/tools/learn.js +10 -8
- package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
- package/dist/tools/registry/auth.js +1 -11
- package/dist/tools/semantic-search-handler.js +5 -6
- package/dist/tools/status-handler.js +19 -6
- package/dist/tools/sync-spec-state-handler.js +49 -1
- package/dist/tools/update-status/batch.d.ts +6 -2
- package/dist/tools/update-status/batch.js +58 -1
- package/dist/tools/update-status/dod-gates.d.ts +16 -1
- package/dist/tools/update-status/dod-gates.js +191 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
- package/dist/tools/update-status/done-receipt-verifier.js +37 -2
- package/dist/tools/update-status/evidence-gate.d.ts +4 -0
- package/dist/tools/update-status/evidence-gate.js +67 -2
- package/dist/tools/update-status/file-sync.d.ts +2 -2
- package/dist/tools/update-status/index.d.ts +23 -1
- package/dist/tools/update-status/index.js +201 -24
- package/dist/tools/update-status/transition-guard.js +13 -1
- package/dist/tools/validation-loop-handler.js +16 -15
- package/dist/tools/workspace-dashboard-handler.js +38 -0
- package/dist/types/evidence-autofill.d.ts +34 -0
- package/dist/types/evidence-autofill.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec/core.d.ts +6 -0
- package/dist/types/spec/inputs.d.ts +7 -2
- package/dist/types/spec-format.d.ts +1 -1
- package/dist/types/spec-language-translation.d.ts +18 -0
- package/dist/types/spec-language-translation.js +5 -0
- package/dist/types/spec-registry.d.ts +0 -2
- package/dist/types/status.d.ts +2 -0
- package/dist/types/transition-log.d.ts +1 -1
- package/dist/types/validation.d.ts +8 -2
- package/dist/types/vector-store.d.ts +18 -0
- package/dist/types/workers.d.ts +0 -3
- package/package.json +11 -10
- package/planu-native.json +8 -29
- package/planu-plugin.json +1 -1
- package/dist/engine/security/cve-refresher.d.ts +0 -12
- package/dist/engine/security/cve-refresher.js +0 -128
|
@@ -11,6 +11,7 @@ import { analyzeProject, checkProjectCompleteness } from '../../engine/analyzer.
|
|
|
11
11
|
import { parseProjectQualityRules } from '../../engine/auditor-claude-md.js';
|
|
12
12
|
import { detectMissingAreas, buildRequestPrompt } from '../../engine/context-merger.js';
|
|
13
13
|
import { detectProjectPrivacyConfig, maybeGenerateEnvExample, buildBeginnerEnvGuide, } from './helpers.js';
|
|
14
|
+
import { buildDefaultPrivacyConfig } from '../../engine/pii-detector.js';
|
|
14
15
|
import { checkBundledVersionGap } from '../../engine/version-detector/bundled-version-checker.js';
|
|
15
16
|
import { checkAndFixBundledVersion } from '../../engine/mcp-config/mcp-config-writer.js';
|
|
16
17
|
import { buildInitProjectResult } from './result-builder.js';
|
|
@@ -86,6 +87,25 @@ function detectMultipleFrameworks(stack) {
|
|
|
86
87
|
}
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* SPEC-1343: Runs one independent, best-effort pipeline stage in isolation so it can be
|
|
92
|
+
* batched with `Promise.all` alongside other independent stages. A rejection never
|
|
93
|
+
* propagates to the caller — it resolves to `fallback` and is flagged via `failed`/`error`
|
|
94
|
+
* so init_project can still succeed and surface the failure in its result summary (AC2).
|
|
95
|
+
*/
|
|
96
|
+
async function runOptionalStage(name, fallback, task) {
|
|
97
|
+
try {
|
|
98
|
+
return { name, value: await task(), failed: false };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
value: fallback,
|
|
104
|
+
failed: true,
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
89
109
|
// eslint-disable-next-line max-lines-per-function
|
|
90
110
|
export async function handleInitProject(params, server) {
|
|
91
111
|
// eslint-disable-next-line max-lines-per-function, complexity
|
|
@@ -256,8 +276,14 @@ export async function handleInitProject(params, server) {
|
|
|
256
276
|
const globalConfig = await globalStore.getGlobalConfig();
|
|
257
277
|
const effectiveLocale = locale ?? globalConfig.defaultLocale;
|
|
258
278
|
const effectiveExperience = experienceLevel ?? globalConfig.defaultExperienceLevel;
|
|
259
|
-
//
|
|
260
|
-
|
|
279
|
+
// SPEC-1343: analyzeProject (full tree scan) and detectStackPatterns are both
|
|
280
|
+
// independent read-only scans of the same project tree — run them concurrently
|
|
281
|
+
// instead of back-to-back so wall time is ~1x the slower stage, not the sum (AC1).
|
|
282
|
+
const [knowledge, stackResult] = await Promise.all([
|
|
283
|
+
analyzeProject(projectPath, projectId, effectiveLocale, effectiveExperience),
|
|
284
|
+
detectStackPatterns(projectPath),
|
|
285
|
+
]);
|
|
286
|
+
const { modernPatterns, dddConfig, agileDetection, orchestratorInfo, composeMultiplatform } = stackResult;
|
|
261
287
|
if (approvedTechnologyContract !== null) {
|
|
262
288
|
knowledge.language = approvedTechnologyContract.language ?? knowledge.language;
|
|
263
289
|
knowledge.framework =
|
|
@@ -330,9 +356,6 @@ export async function handleInitProject(params, server) {
|
|
|
330
356
|
if (hourlyRate !== undefined) {
|
|
331
357
|
await globalStore.updateGlobalConfig({ hourlyRate });
|
|
332
358
|
}
|
|
333
|
-
// SPEC-033: Detect modern architecture patterns and stack info
|
|
334
|
-
const stackResult = await detectStackPatterns(projectPath);
|
|
335
|
-
const { modernPatterns, dddConfig, agileDetection, orchestratorInfo, composeMultiplatform } = stackResult;
|
|
336
359
|
// SPEC-568: Smart tool group activation based on detected stack.
|
|
337
360
|
// Runs before saveKnowledge so the snapshot can be persisted in one write.
|
|
338
361
|
const groupProfileSnapshot = applyStackBasedGroupActivations(knowledge.stack);
|
|
@@ -352,16 +375,54 @@ export async function handleInitProject(params, server) {
|
|
|
352
375
|
addProject(projectPath).catch(() => {
|
|
353
376
|
/* best-effort — never block init_project */
|
|
354
377
|
});
|
|
378
|
+
// SPEC-1343: buildProjectConfig -> runSpecMigrations must stay ordered (config must be
|
|
379
|
+
// written before migrations run against it, and runSpecMigrations is invoked exactly
|
|
380
|
+
// once per run, AC3). That ordered chain is independent of the read-only discovery
|
|
381
|
+
// batch below, so both run concurrently (AC1/AC4).
|
|
355
382
|
// SPEC-131: Build legal profile and generate planu.json / What's New
|
|
356
383
|
// SPEC-313: Pass workMode to persist in planu.json
|
|
357
|
-
const
|
|
384
|
+
const configAndMigrations = (async () => {
|
|
385
|
+
const configResult = await buildProjectConfig(projectPath, projectId, knowledge, workMode);
|
|
386
|
+
// Run spec migrations (v0.30 -> v0.40, legacy paths, prefix normalization)
|
|
387
|
+
const migrations = await runSpecMigrations(projectPath, projectId, knowledge, {
|
|
388
|
+
repositoryMigration: 'initialization',
|
|
389
|
+
});
|
|
390
|
+
return { configResult, migrations };
|
|
391
|
+
})();
|
|
392
|
+
// SPEC-1343: Independent, best-effort discovery stages (skills fetch, privacy
|
|
393
|
+
// detection, health check, convention scan) — none of these feed each other, so they
|
|
394
|
+
// batch into a single Promise.all with per-stage error isolation. A failing optional
|
|
395
|
+
// stage falls back to a safe default instead of failing init_project (AC2).
|
|
396
|
+
const [recommendedSkillsOutcome, privacyConfigOutcome, healthReportOutcome, conventionScanOutcome,] = await Promise.all([
|
|
397
|
+
runOptionalStage('fetchRecommendedSkills', [], () => fetchRecommendedSkills(knowledge)),
|
|
398
|
+
runOptionalStage('detectProjectPrivacyConfig', buildDefaultPrivacyConfig('GDPR', []), () => detectProjectPrivacyConfig(projectPath)),
|
|
399
|
+
runOptionalStage('runHealthCheckWithBaseline', null, async () => {
|
|
400
|
+
const allSpecs = await listProjectSpecs(projectId);
|
|
401
|
+
const specSummaries = allSpecs.map((s) => ({
|
|
402
|
+
specId: s.id,
|
|
403
|
+
status: s.status,
|
|
404
|
+
gitBranch: s.gitBranch,
|
|
405
|
+
updatedAt: s.updatedAt,
|
|
406
|
+
}));
|
|
407
|
+
return runHealthCheckWithBaseline(projectPath, projectId, knowledge, specSummaries);
|
|
408
|
+
}),
|
|
409
|
+
runOptionalStage('runConventionScanSafe', 'failed', () => runConventionScanSafe(projectPath)),
|
|
410
|
+
]);
|
|
411
|
+
const recommendedSkills = recommendedSkillsOutcome.value;
|
|
412
|
+
const privacyConfig = privacyConfigOutcome.value;
|
|
413
|
+
const healthReport = healthReportOutcome.value;
|
|
414
|
+
const conventionScanStatus = conventionScanOutcome.value;
|
|
415
|
+
const optionalStageFailures = [
|
|
416
|
+
recommendedSkillsOutcome,
|
|
417
|
+
privacyConfigOutcome,
|
|
418
|
+
healthReportOutcome,
|
|
419
|
+
conventionScanOutcome,
|
|
420
|
+
]
|
|
421
|
+
.filter((outcome) => outcome.failed)
|
|
422
|
+
.map((outcome) => `${outcome.name}: ${outcome.error ?? 'unknown error'}`);
|
|
423
|
+
const { configResult, migrations } = await configAndMigrations;
|
|
358
424
|
const { planuConfigPath, planuConfigGenerated, whatsNew } = configResult;
|
|
359
|
-
|
|
360
|
-
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = await runSpecMigrations(projectPath, projectId, knowledge, {
|
|
361
|
-
repositoryMigration: 'initialization',
|
|
362
|
-
});
|
|
363
|
-
// Auto-fetch recommended skills based on detected stack
|
|
364
|
-
const recommendedSkills = await fetchRecommendedSkills(knowledge);
|
|
425
|
+
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = migrations;
|
|
365
426
|
// Auto-install or queue skills (opt-out via planu.json or input param, SPEC-185)
|
|
366
427
|
const autoInstallFromConfig = autoInstallSkills ?? (await readAutoInstallFlag(planuConfigPath));
|
|
367
428
|
const { skillsAutoInstalled, skillsPendingInstall, skillsSkipped } = await orchestrateSkillInstalls(recommendedSkills, projectPath, autoInstallFromConfig);
|
|
@@ -393,17 +454,6 @@ export async function handleInitProject(params, server) {
|
|
|
393
454
|
const beginnerEnvGuide = effectiveExperience === 'beginner' && envVars.length > 0
|
|
394
455
|
? buildBeginnerEnvGuide(envVars)
|
|
395
456
|
: null;
|
|
396
|
-
// SPEC-030: Detect legal framework and third-party data processors
|
|
397
|
-
const privacyConfig = await detectProjectPrivacyConfig(projectPath);
|
|
398
|
-
// SPEC-180: Project health check (best-effort)
|
|
399
|
-
const allSpecs = await listProjectSpecs(projectId);
|
|
400
|
-
const specSummaries = allSpecs.map((s) => ({
|
|
401
|
-
specId: s.id,
|
|
402
|
-
status: s.status,
|
|
403
|
-
gitBranch: s.gitBranch,
|
|
404
|
-
updatedAt: s.updatedAt,
|
|
405
|
-
}));
|
|
406
|
-
const healthReport = await runHealthCheckWithBaseline(projectPath, projectId, knowledge, specSummaries);
|
|
407
457
|
// SPEC-491: Auto-cleanup planu/ — remove legacy HTMLs, migrate to lean spec format (fire-and-forget)
|
|
408
458
|
import('../../engine/spec-migrator/planu-root-cleaner.js')
|
|
409
459
|
.then(({ cleanPlanuRoot }) => cleanPlanuRoot(join(projectPath, 'planu')))
|
|
@@ -414,8 +464,6 @@ export async function handleInitProject(params, server) {
|
|
|
414
464
|
void withAudit(projectPath, 'init_project', 'regeneratePages', () => regeneratePages(projectPath, ['knowledge', 'risks', 'decisions']), (pages) => ({ pages: pages.length })).catch(() => {
|
|
415
465
|
/* best-effort — never fail init */
|
|
416
466
|
});
|
|
417
|
-
// SPEC-228: Deep scan — detect conventions and write planu/conventions.json (best-effort)
|
|
418
|
-
const conventionScanStatus = await runConventionScanSafe(projectPath);
|
|
419
467
|
// SPEC-645: Detect LLM client and cache in conventions.json (fire-and-forget)
|
|
420
468
|
void import('../../engine/client-detection.js')
|
|
421
469
|
.then(({ detectAndCacheClient }) => detectAndCacheClient(projectPath))
|
|
@@ -480,6 +528,11 @@ export async function handleInitProject(params, server) {
|
|
|
480
528
|
}
|
|
481
529
|
// SPEC-469: Build autopilot summary from setup steps performed
|
|
482
530
|
const collector = new AutopilotSummaryCollector();
|
|
531
|
+
// SPEC-1343 AC2: surface optional discovery-stage failures in the result summary
|
|
532
|
+
// instead of failing init_project.
|
|
533
|
+
for (const failure of optionalStageFailures) {
|
|
534
|
+
collector.pushFail('discovery-stage-failed', failure);
|
|
535
|
+
}
|
|
483
536
|
if (knowledge.stack.length > 0) {
|
|
484
537
|
collector.pushOk('stack-detection', `Stack detected: ${knowledge.stack.join(', ')}`);
|
|
485
538
|
}
|
package/dist/tools/learn.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// to pattern-store for future use by estimation, auditing, and spec generation.
|
|
4
4
|
// SPEC-011: adds dispatchFeedbackEvent for feedback loop dispatch.
|
|
5
5
|
import { patternStore, knowledgeStore } from '../storage/index.js';
|
|
6
|
+
import { SemanticIndexStore } from '../storage/semantic-index-store.js';
|
|
6
7
|
import { t, ti } from '../i18n/index.js';
|
|
7
|
-
import { TFIDFEngine } from '../engine/vector-store/tfidf.js';
|
|
8
8
|
import { cosineSimilarity } from '../engine/vector-store/similarity.js';
|
|
9
9
|
import { compactJson } from './output-formatter.js';
|
|
10
10
|
/** Default cosine similarity threshold for semantic deduplication. */
|
|
@@ -45,7 +45,7 @@ export async function handleLearn(args) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
// 3. Semantic deduplication check (SPEC-075 AC-08)
|
|
48
|
-
const dedupWarning = checkSemanticDuplicate(existingPatterns, pattern, type);
|
|
48
|
+
const dedupWarning = await checkSemanticDuplicate(projectId, existingPatterns, pattern, type);
|
|
49
49
|
// 4. Create new pattern (store even if semantically similar — just warn)
|
|
50
50
|
const patternId = `PAT-${type.substring(0, 3).toUpperCase()}-${Date.now().toString(36).toUpperCase()}`;
|
|
51
51
|
const learnedPattern = {
|
|
@@ -93,18 +93,20 @@ export async function handleLearn(args) {
|
|
|
93
93
|
/**
|
|
94
94
|
* SPEC-075 AC-08: Check existing patterns for semantic similarity using TF-IDF.
|
|
95
95
|
* Returns a DedupWarning if a pattern with cosine similarity > threshold exists.
|
|
96
|
+
* SPEC-1345: the same-type corpus is loaded from a persisted, incrementally
|
|
97
|
+
* updated TF-IDF index instead of being rebuilt from scratch on every call.
|
|
96
98
|
*/
|
|
97
|
-
function checkSemanticDuplicate(existingPatterns, newPattern, patternType, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
99
|
+
async function checkSemanticDuplicate(projectId, existingPatterns, newPattern, patternType, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
98
100
|
// Filter to same-type patterns only
|
|
99
101
|
const sameType = existingPatterns.filter((p) => p.type === patternType);
|
|
100
102
|
if (sameType.length === 0) {
|
|
101
103
|
return undefined;
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
const tfidf =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
const indexStore = new SemanticIndexStore(projectId, `pattern-${patternType}`);
|
|
106
|
+
const tfidf = await indexStore.loadOrBuild(sameType.map((p) => ({ id: p.id, content: p.pattern })));
|
|
107
|
+
// Embed the candidate in-memory only (it isn't stored yet, so it never
|
|
108
|
+
// becomes part of the persisted corpus) so the IDF reflects it, matching
|
|
109
|
+
// the previous non-persisted behavior.
|
|
108
110
|
tfidf.addDocument(newPattern);
|
|
109
111
|
// Embed the new pattern
|
|
110
112
|
const newEmbedding = tfidf.embed(newPattern);
|
|
@@ -359,18 +359,18 @@ export function registerCoreSpecTools(server) {
|
|
|
359
359
|
.describe('Set true when implementation drift requires reconcile_spec before status=done; true blocks done.'),
|
|
360
360
|
actuals: z
|
|
361
361
|
.object({
|
|
362
|
-
devHours: z.number().min(0),
|
|
363
|
-
reviewHours: z.number().min(0),
|
|
364
|
-
tokensOpus: z.number().min(0),
|
|
365
|
-
tokensSonnet: z.number().min(0),
|
|
366
|
-
apiCostUsd: z.number().min(0),
|
|
367
|
-
humanCostUsd: z.number().min(0),
|
|
368
|
-
totalCostUsd: z.number().min(0),
|
|
369
|
-
completedAt: z.string().max(500),
|
|
370
|
-
notes: z.string().max(10_000),
|
|
362
|
+
devHours: z.number().min(0).optional(),
|
|
363
|
+
reviewHours: z.number().min(0).optional(),
|
|
364
|
+
tokensOpus: z.number().min(0).optional(),
|
|
365
|
+
tokensSonnet: z.number().min(0).optional(),
|
|
366
|
+
apiCostUsd: z.number().min(0).optional(),
|
|
367
|
+
humanCostUsd: z.number().min(0).optional(),
|
|
368
|
+
totalCostUsd: z.number().min(0).optional(),
|
|
369
|
+
completedAt: z.string().max(500).optional(),
|
|
370
|
+
notes: z.string().max(10_000).optional(),
|
|
371
371
|
})
|
|
372
372
|
.optional()
|
|
373
|
-
.describe('Actual metrics
|
|
373
|
+
.describe('Actual metrics for status = done. All fields optional (SPEC-1356): missing numeric fields default to 0, completedAt/notes are auto-filled when omitted.'),
|
|
374
374
|
reviewNotes: z
|
|
375
375
|
.string()
|
|
376
376
|
.max(10_000)
|
|
@@ -407,7 +407,8 @@ export function registerCoreSpecTools(server) {
|
|
|
407
407
|
implementationReviewDigest: z
|
|
408
408
|
.string()
|
|
409
409
|
.regex(/^sha256:[a-f0-9]{64}$/u)
|
|
410
|
-
.optional()
|
|
410
|
+
.optional()
|
|
411
|
+
.describe('SHA-256 of the exact validation-report.json bytes. Required implementation evidence for status=done; for status=review it is valid only inside the complete trusted local-MCP reconciliation tuple.'),
|
|
411
412
|
forceApprove: z
|
|
412
413
|
.boolean()
|
|
413
414
|
.optional()
|
|
@@ -424,7 +425,7 @@ export function registerCoreSpecTools(server) {
|
|
|
424
425
|
.max(4096)
|
|
425
426
|
.optional()
|
|
426
427
|
.describe('Absolute project root. Preferred when projectId is unknown.'),
|
|
427
|
-
status: SpecStatusEnum.describe('New status for all specs'),
|
|
428
|
+
status: SpecStatusEnum.exclude(['done']).describe('New status for all specs. Batch supports draft, review, approved, implementing, and discarded; close done specs individually with full evidence.'),
|
|
428
429
|
dryRun: z.boolean().optional().describe('Preview the batch without mutating any spec.'),
|
|
429
430
|
reviewNotes: z
|
|
430
431
|
.string()
|
|
@@ -22,8 +22,6 @@ export async function handleRegistryLogin(args) {
|
|
|
22
22
|
const token = args.token;
|
|
23
23
|
const user = args.user;
|
|
24
24
|
const email = args.email;
|
|
25
|
-
const rawPlan = args.plan;
|
|
26
|
-
const plan = rawPlan ?? 'free';
|
|
27
25
|
const expiresAt = args.expiresAt;
|
|
28
26
|
if (!token || !user || !email) {
|
|
29
27
|
return {
|
|
@@ -37,7 +35,6 @@ export async function handleRegistryLogin(args) {
|
|
|
37
35
|
token: await storeSecretField('global/registry/pat', 'registry-pat', token),
|
|
38
36
|
user,
|
|
39
37
|
email,
|
|
40
|
-
plan,
|
|
41
38
|
expiresAt: expiresAt ?? new Date(Date.now() + TOKEN_EXPIRY_MS).toISOString(),
|
|
42
39
|
};
|
|
43
40
|
const credPath = credentialsPath();
|
|
@@ -45,12 +42,7 @@ export async function handleRegistryLogin(args) {
|
|
|
45
42
|
await chmod(resolveStorageLayout().secrets, 0o700);
|
|
46
43
|
await writeFile(credPath, JSON.stringify(credentials, null, 2), 'utf-8');
|
|
47
44
|
await chmod(credPath, 0o600);
|
|
48
|
-
return compactResult([
|
|
49
|
-
`Done: Logged in successfully`,
|
|
50
|
-
`**user**: ${user}`,
|
|
51
|
-
`**email**: ${email}`,
|
|
52
|
-
`**plan**: ${plan}`,
|
|
53
|
-
].join('\n'));
|
|
45
|
+
return compactResult([`Done: Logged in successfully`, `**user**: ${user}`, `**email**: ${email}`].join('\n'));
|
|
54
46
|
}
|
|
55
47
|
catch (error) {
|
|
56
48
|
/* v8 ignore start -- unexpected runtime errors */
|
|
@@ -100,7 +92,6 @@ export async function handleRegistryWhoami(_args) {
|
|
|
100
92
|
const info = {
|
|
101
93
|
user: cred.user,
|
|
102
94
|
email: cred.email,
|
|
103
|
-
plan: cred.plan,
|
|
104
95
|
expiresAt: cred.expiresAt,
|
|
105
96
|
expired: isExpired,
|
|
106
97
|
authenticated: !isExpired,
|
|
@@ -108,7 +99,6 @@ export async function handleRegistryWhoami(_args) {
|
|
|
108
99
|
return compactResult([
|
|
109
100
|
`**user**: ${info.user}`,
|
|
110
101
|
`**email**: ${info.email}`,
|
|
111
|
-
`**plan**: ${info.plan}`,
|
|
112
102
|
`**expiresAt**: ${info.expiresAt}`,
|
|
113
103
|
`**expired**: ${String(info.expired)}`,
|
|
114
104
|
`**authenticated**: ${String(info.authenticated)}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tools/semantic-search-handler.ts — Handler for semantic_search tool.
|
|
2
2
|
// SPEC-075 AC-07: Generates TF-IDF embedding of query, searches backend.
|
|
3
3
|
import { getOrCreateBackend } from '../storage/vector-store/backend-factory.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SemanticIndexStore } from '../storage/semantic-index-store.js';
|
|
5
5
|
/**
|
|
6
6
|
* Handle a semantic_search request.
|
|
7
7
|
* Embeds the query with TF-IDF, then searches the vector backend.
|
|
@@ -33,11 +33,10 @@ export async function handleSemanticSearch(args) {
|
|
|
33
33
|
],
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
36
|
+
// SPEC-1345: load the persisted TF-IDF index for this scope, incrementally
|
|
37
|
+
// reconciled against the current corpus (no full rebuild for unchanged docs).
|
|
38
|
+
const indexStore = new SemanticIndexStore(projectId, scope);
|
|
39
|
+
const tfidf = await indexStore.loadOrBuild(allDocs.map((doc) => ({ id: doc.id, content: doc.content })));
|
|
41
40
|
// Embed the query
|
|
42
41
|
const queryEmbedding = tfidf.embed(query);
|
|
43
42
|
// Search
|
|
@@ -79,18 +79,28 @@ function parseSpecNumber(id) {
|
|
|
79
79
|
const match = /SPEC-(\d+)/i.exec(id);
|
|
80
80
|
return match?.[1] !== undefined ? parseInt(match[1], 10) : 0;
|
|
81
81
|
}
|
|
82
|
+
function selectActiveSpec(specs) {
|
|
83
|
+
// Lifecycle-first ordering: implementing always outranks review, regardless of SPEC number.
|
|
84
|
+
// Within the same lifecycle status, the tie-break is descending SPEC number (most recent first).
|
|
85
|
+
for (const status of ['implementing', 'review']) {
|
|
86
|
+
const candidates = specs
|
|
87
|
+
.filter((s) => s.status === status)
|
|
88
|
+
.sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
|
|
89
|
+
const candidate = candidates[0];
|
|
90
|
+
if (candidate !== undefined) {
|
|
91
|
+
return { id: candidate.id, title: candidate.title, status };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
82
96
|
async function getStatusSpecSnapshot(projectId) {
|
|
83
97
|
try {
|
|
84
98
|
const specs = await listSpecs(projectId);
|
|
85
|
-
const
|
|
86
|
-
.filter((s) => s.status === 'implementing' || s.status === 'review')
|
|
87
|
-
.sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
|
|
99
|
+
const active = selectActiveSpec(specs.filter((s) => s.status === 'implementing' || s.status === 'review'));
|
|
88
100
|
const approved = specs
|
|
89
101
|
.filter((s) => s.status === 'approved')
|
|
90
102
|
.sort((a, b) => parseSpecNumber(a.id) - parseSpecNumber(b.id));
|
|
91
|
-
const activeItem = inProgress[0];
|
|
92
103
|
const nextItem = approved[0];
|
|
93
|
-
const active = activeItem !== undefined ? { id: activeItem.id, title: activeItem.title } : null;
|
|
94
104
|
const next = nextItem !== undefined ? { id: nextItem.id, title: nextItem.title } : null;
|
|
95
105
|
return { active, next, queueCount: approved.length };
|
|
96
106
|
}
|
|
@@ -130,6 +140,9 @@ async function getSLABreaches(projectId) {
|
|
|
130
140
|
// ---------------------------------------------------------------------------
|
|
131
141
|
function buildSuggestion(snapshot) {
|
|
132
142
|
if (snapshot.active !== null) {
|
|
143
|
+
if (snapshot.active.status === 'review') {
|
|
144
|
+
return `Resolve review for ${snapshot.active.id} — it is not yet implementing`;
|
|
145
|
+
}
|
|
133
146
|
return `Continue ${snapshot.active.id} — it may be blocking others`;
|
|
134
147
|
}
|
|
135
148
|
if (snapshot.next !== null) {
|
|
@@ -145,7 +158,7 @@ function buildOutput(params) {
|
|
|
145
158
|
const lines = ['Planu Status', '━━━━━━━━━━━━━━━'];
|
|
146
159
|
if (snapshot.active !== null) {
|
|
147
160
|
const title = snapshot.active.title.slice(0, 40);
|
|
148
|
-
lines.push(`ACTIVE ${snapshot.active.id} ${title} (
|
|
161
|
+
lines.push(`ACTIVE ${snapshot.active.id} ${title} (${snapshot.active.status})`);
|
|
149
162
|
}
|
|
150
163
|
else {
|
|
151
164
|
lines.push(`ACTIVE (none)`);
|
|
@@ -12,7 +12,7 @@ import { parseFrontmatter } from '../engine/frontmatter-parser.js';
|
|
|
12
12
|
import { verifyTerminalFrontmatter } from '../engine/frontmatter-sha/index.js';
|
|
13
13
|
import { appendTransitionEvent } from '../storage/transition-log.js';
|
|
14
14
|
import { cleanEphemeralArtifacts } from '../engine/housekeeping/index.js';
|
|
15
|
-
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Sync helpers (exported for startup use)
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
@@ -33,9 +33,15 @@ export async function syncSpecState(projectPath, projectId) {
|
|
|
33
33
|
divergences: [],
|
|
34
34
|
errors: [`Failed to load specs for ${projectId}: ${msg}`],
|
|
35
35
|
rejected: [],
|
|
36
|
+
ghosts: [],
|
|
36
37
|
};
|
|
37
38
|
}
|
|
39
|
+
// SPEC-1351 (AC3): quarantine store entries whose spec.md is missing on
|
|
40
|
+
// disk — ghost registrations left behind by a create_spec attempt that
|
|
41
|
+
// reported success but never persisted (or whose file was later deleted).
|
|
42
|
+
const ghosts = await detectGhostSpecs(projectId, dataEntries);
|
|
38
43
|
const { updates, report } = await detectSpecStateDivergences(projectPath, dataEntries);
|
|
44
|
+
report.ghosts = ghosts;
|
|
39
45
|
// SyncReport.rejected is always defined (SPEC-720)
|
|
40
46
|
const TERMINAL_STATUSES = ['done', 'discarded'];
|
|
41
47
|
for (const { specId, newStatus } of updates) {
|
|
@@ -136,6 +142,41 @@ export async function syncSpecState(projectPath, projectId) {
|
|
|
136
142
|
}
|
|
137
143
|
return report;
|
|
138
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* SPEC-1351 (AC3): scan the data store for entries whose spec.md is missing on
|
|
147
|
+
* disk and quarantine each one — appends a `ghost_spec_quarantined` transition
|
|
148
|
+
* event (surfaced by workspace_alerts with a recovery action) and returns the
|
|
149
|
+
* list so callers can report it inline.
|
|
150
|
+
*/
|
|
151
|
+
async function detectGhostSpecs(projectId, dataEntries) {
|
|
152
|
+
const ghosts = [];
|
|
153
|
+
await Promise.all(dataEntries.map(async (entry) => {
|
|
154
|
+
if (!entry.specPath) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const fileStat = await stat(entry.specPath);
|
|
159
|
+
if (fileStat.size > 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// fall through to quarantine — missing or unreadable spec.md
|
|
165
|
+
}
|
|
166
|
+
const reason = `Store entry ${entry.id} references spec.md at ${entry.specPath}, which is missing or empty on disk. Recreate the spec via create_spec or restore spec.md from git/backup.`;
|
|
167
|
+
ghosts.push({ specId: entry.id, specPath: entry.specPath, reason });
|
|
168
|
+
void appendTransitionEvent({
|
|
169
|
+
projectId,
|
|
170
|
+
specId: entry.id,
|
|
171
|
+
eventType: 'ghost_spec_quarantined',
|
|
172
|
+
actor: 'sync_spec_state',
|
|
173
|
+
reason,
|
|
174
|
+
}).catch(() => {
|
|
175
|
+
/* best-effort */
|
|
176
|
+
});
|
|
177
|
+
}));
|
|
178
|
+
return ghosts;
|
|
179
|
+
}
|
|
139
180
|
/**
|
|
140
181
|
* Startup sync: iterates all registered projects and calls syncSpecState for each.
|
|
141
182
|
* Non-blocking — errors are logged but not propagated.
|
|
@@ -183,6 +224,13 @@ function formatReport(report, label) {
|
|
|
183
224
|
lines.push(` - ${r.specId}: ${r.reason}`);
|
|
184
225
|
}
|
|
185
226
|
}
|
|
227
|
+
// SPEC-1351: Ghost store entries quarantined (missing spec.md on disk)
|
|
228
|
+
if ((report.ghosts?.length ?? 0) > 0) {
|
|
229
|
+
lines.push(' Quarantined ghosts (spec.md missing — recreate or restore):');
|
|
230
|
+
for (const g of report.ghosts ?? []) {
|
|
231
|
+
lines.push(` - ${g.specId}: ${g.specPath}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
186
234
|
if (report.errors.length > 0) {
|
|
187
235
|
lines.push(' Errors:');
|
|
188
236
|
for (const e of report.errors) {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import type { ToolResult, UpdateStatusBatchInput } from '../../types/index.js';
|
|
2
|
-
|
|
1
|
+
import type { SpecStatus, ToolResult, UpdateStatusBatchInput } from '../../types/index.js';
|
|
2
|
+
type UncheckedUpdateStatusBatchInput = Omit<UpdateStatusBatchInput, 'status'> & {
|
|
3
|
+
status: SpecStatus;
|
|
4
|
+
};
|
|
5
|
+
export declare function handleUpdateStatusBatch(input: UncheckedUpdateStatusBatchInput): Promise<ToolResult>;
|
|
6
|
+
export {};
|
|
3
7
|
//# sourceMappingURL=batch.d.ts.map
|
|
@@ -6,7 +6,60 @@ function firstText(result) {
|
|
|
6
6
|
const first = result.content[0];
|
|
7
7
|
return first?.type === 'text' ? first.text : JSON.stringify(result.structuredContent ?? {});
|
|
8
8
|
}
|
|
9
|
+
/** SPEC-1349: extract the gate failure code + actionable next step from a per-spec transition result. */
|
|
10
|
+
function failureDetails(result) {
|
|
11
|
+
const structured = result.structuredContent;
|
|
12
|
+
const code = typeof structured?.error === 'string' ? structured.error : undefined;
|
|
13
|
+
const nextAction = typeof structured?.fixHint === 'string' ? structured.fixHint : undefined;
|
|
14
|
+
return { code, nextAction };
|
|
15
|
+
}
|
|
16
|
+
const MAX_NARRATIVE_FAILURE_ROWS = 5;
|
|
17
|
+
/** SPEC-1349: render up to MAX_NARRATIVE_FAILURE_ROWS per-spec reasons, capping token cost. */
|
|
18
|
+
function renderFailureNarrative(failed) {
|
|
19
|
+
if (failed.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const shown = failed.slice(0, MAX_NARRATIVE_FAILURE_ROWS);
|
|
23
|
+
const remaining = failed.length - shown.length;
|
|
24
|
+
const lines = ['Failed specs:', ...shown.map((row) => `- ${row.specId}: ${row.message}`)];
|
|
25
|
+
if (remaining > 0) {
|
|
26
|
+
const dominantCode = shown[0]?.code;
|
|
27
|
+
const sameCause = dominantCode !== undefined && failed.every((row) => row.code === dominantCode);
|
|
28
|
+
lines.push(sameCause
|
|
29
|
+
? `...and ${String(remaining)} more with the same cause.`
|
|
30
|
+
: `...and ${String(remaining)} more (see structuredContent.failed for details).`);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
/** SPEC-1349: when every failure shares one gate cause, surface a single aggregated next step. */
|
|
35
|
+
function dominantFailureNextAction(failed) {
|
|
36
|
+
if (failed.length === 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const firstCode = failed[0]?.code;
|
|
40
|
+
if (firstCode === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const allSameCause = failed.every((row) => row.code === firstCode);
|
|
44
|
+
if (!allSameCause) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return failed[0]?.nextAction;
|
|
48
|
+
}
|
|
9
49
|
export async function handleUpdateStatusBatch(input) {
|
|
50
|
+
if (input.status === 'done') {
|
|
51
|
+
const fixHint = 'Close each spec individually with update_status(done) and its full implementation review evidence.';
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: `BATCH_DONE_UNSUPPORTED: Batch done transitions are not supported.\nFix: ${fixHint}`,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
isError: true,
|
|
60
|
+
structuredContent: { error: 'BATCH_DONE_UNSUPPORTED', code: 422, fixHint },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
10
63
|
const uniqueSpecIds = [...new Set(input.specIds)].sort();
|
|
11
64
|
if (uniqueSpecIds.length === 0) {
|
|
12
65
|
return {
|
|
@@ -57,7 +110,7 @@ export async function handleUpdateStatusBatch(input) {
|
|
|
57
110
|
skipped.push({ specId, message });
|
|
58
111
|
}
|
|
59
112
|
else {
|
|
60
|
-
failed.push({ specId, message });
|
|
113
|
+
failed.push({ specId, message, ...failureDetails(result) });
|
|
61
114
|
}
|
|
62
115
|
}
|
|
63
116
|
else {
|
|
@@ -81,11 +134,14 @@ export async function handleUpdateStatusBatch(input) {
|
|
|
81
134
|
/* reliability-cleanup: POST_BATCH_STRICT_CLEANUP — validation remains fail-closed */
|
|
82
135
|
reportClassifiedDegradation('POST_BATCH_STRICT_CLEANUP', error);
|
|
83
136
|
}
|
|
137
|
+
const aggregatedNextAction = dominantFailureNextAction(failed);
|
|
84
138
|
const text = [
|
|
85
139
|
`Batch status transition complete: ${input.status}`,
|
|
86
140
|
`Updated: ${updated.length}`,
|
|
87
141
|
`Skipped: ${skipped.length}`,
|
|
88
142
|
`Failed: ${failed.length}`,
|
|
143
|
+
...renderFailureNarrative(failed),
|
|
144
|
+
...(aggregatedNextAction ? [`Next action: ${aggregatedNextAction}`] : []),
|
|
89
145
|
].join('\n');
|
|
90
146
|
return {
|
|
91
147
|
content: [{ type: 'text', text }],
|
|
@@ -95,6 +151,7 @@ export async function handleUpdateStatusBatch(input) {
|
|
|
95
151
|
skipped,
|
|
96
152
|
failed,
|
|
97
153
|
sideEffectsFlushed: ['strict-planu-cleanup'],
|
|
154
|
+
...(aggregatedNextAction ? { nextAction: aggregatedNextAction } : {}),
|
|
98
155
|
},
|
|
99
156
|
};
|
|
100
157
|
}
|
|
@@ -83,6 +83,21 @@ export interface DoneGateResult {
|
|
|
83
83
|
forcedBypassWarning: string | null;
|
|
84
84
|
freshnessLease: ValidationFreshnessLease | null;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* SPEC-1356 (T-A1): auto-derive the sha256 digest of the persisted
|
|
88
|
+
* `validation-report.json` so callers no longer have to compute it by hand.
|
|
89
|
+
* Reads and hashes the exact same bytes `checkDoneReviewDigest` would validate
|
|
90
|
+
* against, using the same hardened path/identity checks. Returns the digest, or
|
|
91
|
+
* the exact blocking `ToolResult` when the report cannot be safely read (e.g.
|
|
92
|
+
* validate has not run yet). Exported for direct unit testing and for the
|
|
93
|
+
* orchestration layer (`update-status/index.ts`) that no longer requires the
|
|
94
|
+
* caller to supply `implementationReviewDigest`.
|
|
95
|
+
*/
|
|
96
|
+
export declare function deriveDoneReviewDigest(specId: string, projectId: string): Promise<{
|
|
97
|
+
digest: string;
|
|
98
|
+
} | {
|
|
99
|
+
error: ToolResult;
|
|
100
|
+
}>;
|
|
86
101
|
/**
|
|
87
102
|
* SPEC-725: Check the validation-report artifact gate before marking done.
|
|
88
103
|
* SPEC-1050: Fail closed. A done transition requires a fresh validation-report
|
|
@@ -99,7 +114,7 @@ export declare function checkSpecReviewGate(specId: string, projectId: string, _
|
|
|
99
114
|
* When force=true, gates are not blocking but failing items are recorded in
|
|
100
115
|
* the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
|
|
101
116
|
*/
|
|
102
|
-
export declare function checkDoneGates(spec: Spec, specId: string, projectId: string, projectPath: string | undefined, _force: boolean | undefined, _forceReason?: string, verifyReceipt?: DoneReceiptVerifier): Promise<DoneGateResult>;
|
|
117
|
+
export declare function checkDoneGates(spec: Spec, specId: string, projectId: string, projectPath: string | undefined, _force: boolean | undefined, _forceReason?: string, verifyReceipt?: DoneReceiptVerifier, implementationReviewDigest?: string): Promise<DoneGateResult>;
|
|
103
118
|
export { checkQaGate } from './qa-gate.js';
|
|
104
119
|
export { checkApprovedFormatGate, checkComplianceGate, type ComplianceGateResult, } from './approval-gates.js';
|
|
105
120
|
/**
|