vibe-coding-master 0.9.9 → 0.9.11
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/dist/backend/gateway/gateway-service.js +17 -0
- package/dist/backend/server.js +2 -1
- package/dist/backend/services/artifact-service.js +6 -0
- package/dist/backend/services/auto-memory-service.js +21 -2
- package/dist/backend/services/claude-hook-service.js +88 -2
- package/dist/backend/services/translation-service.js +8 -3
- package/dist/backend/services/usage-limit.js +133 -0
- package/dist/backend/services/workflow-control-service.js +8 -3
- package/dist/backend/templates/handoff.js +3 -1
- package/dist/backend/templates/harness/architect-agent.js +2 -0
- package/dist/backend/templates/harness/claude-root.js +1 -0
- package/dist/backend/templates/harness/project-manager-agent.js +1 -0
- package/dist/shared/validation/artifact-check.js +19 -5
- package/dist/shared/validation/artifact-contract.js +11 -0
- package/dist-frontend/assets/{index-DG2WfNXT.js → index-B9DTE6aF.js} +33 -33
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -1346,6 +1346,22 @@ export function createGatewayService(deps) {
|
|
|
1346
1346
|
}
|
|
1347
1347
|
function formatGatewayRoleStopFailure(input, round) {
|
|
1348
1348
|
const roundLabel = round?.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
|
|
1349
|
+
const claudeMessage = input.lastAssistantMessage && input.lastAssistantMessage !== input.errorDetails
|
|
1350
|
+
? `Claude Code: ${truncateGatewayFailureDetails(input.lastAssistantMessage)}`
|
|
1351
|
+
: undefined;
|
|
1352
|
+
if (input.usageLimitWait) {
|
|
1353
|
+
return [
|
|
1354
|
+
"Claude 额度已用完,VCM 已暂停该角色,等待额度恢复。",
|
|
1355
|
+
`Task: ${input.taskSlug}`,
|
|
1356
|
+
`Role: ${input.role}`,
|
|
1357
|
+
`Round: ${roundLabel}`,
|
|
1358
|
+
claudeMessage,
|
|
1359
|
+
"",
|
|
1360
|
+
input.usageLimitWait.resetAt
|
|
1361
|
+
? "VCM 会在额度恢复约 2 分钟后自动继续,无需操作。"
|
|
1362
|
+
: "Claude Code 没有给出恢复时间,VCM 每小时自动检查一次,恢复后自动继续。"
|
|
1363
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1364
|
+
}
|
|
1349
1365
|
const retryLine = input.maxAttempts !== undefined
|
|
1350
1366
|
? `Retry: ${input.attempt ?? 0}/${input.maxAttempts}`
|
|
1351
1367
|
: undefined;
|
|
@@ -1357,6 +1373,7 @@ export function createGatewayService(deps) {
|
|
|
1357
1373
|
input.error ? `Reason: ${input.error}` : undefined,
|
|
1358
1374
|
retryLine,
|
|
1359
1375
|
input.errorDetails ? `Details: ${truncateGatewayFailureDetails(input.errorDetails)}` : undefined,
|
|
1376
|
+
claudeMessage,
|
|
1360
1377
|
"",
|
|
1361
1378
|
"请在 VCM 中检查状态,或修复问题后继续给出下一步指令。"
|
|
1362
1379
|
].filter((line) => line !== undefined).join("\n");
|
package/dist/backend/server.js
CHANGED
|
@@ -276,7 +276,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
276
276
|
const artifactService = createArtifactService(fs, {
|
|
277
277
|
workflowControlService,
|
|
278
278
|
getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role),
|
|
279
|
-
getCompletedDurableDocAssignment: (taskRepoRoot, role, artifactPath) => autoMemoryService.getCompletedDurableDocAssignment(taskRepoRoot, role, artifactPath)
|
|
279
|
+
getCompletedDurableDocAssignment: (taskRepoRoot, role, artifactPath) => autoMemoryService.getCompletedDurableDocAssignment(taskRepoRoot, role, artifactPath),
|
|
280
|
+
checkDurableDocReport: (taskRepoRoot, assignment, reportContent) => autoMemoryService.checkDurableDocAssignmentReport(taskRepoRoot, assignment, reportContent)
|
|
280
281
|
});
|
|
281
282
|
const projectService = createProjectService({
|
|
282
283
|
fs,
|
|
@@ -240,6 +240,12 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
240
240
|
if (validation.errors.length > 0) {
|
|
241
241
|
throw artifactRejected(input.kind, validation.errors);
|
|
242
242
|
}
|
|
243
|
+
if (assignment && input.mode === "final") {
|
|
244
|
+
const problem = await deps.checkDurableDocReport?.(input.repoRoot, assignment, normalized);
|
|
245
|
+
if (problem) {
|
|
246
|
+
throw artifactRejected(input.kind, [problem]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
243
249
|
if (input.kind === "workflow-progress") {
|
|
244
250
|
if (input.mode !== "final") {
|
|
245
251
|
throw artifactRejected(input.kind, ["Workflow Progress must be submitted in final mode."]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { checkMarkdownArtifact, readArtifactSectionContent, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
4
|
-
import { DOCS_UPDATE_COMMIT_RULE } from "../../shared/validation/artifact-contract.js";
|
|
4
|
+
import { DOCS_UPDATE_COMMIT_RULE, durableDocCommitSubjectPrefix } from "../../shared/validation/artifact-contract.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { VcmError } from "../errors.js";
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -383,6 +383,18 @@ export function createAutoMemoryService(deps) {
|
|
|
383
383
|
const assignment = state?.status === "documenting" ? currentDurableDocAssignment(state) : undefined;
|
|
384
384
|
return assignment?.status === "running" && assignment.owner === role ? assignment : undefined;
|
|
385
385
|
}
|
|
386
|
+
async function checkDurableDocAssignmentReport(taskRepoRoot, assignment, reportContent) {
|
|
387
|
+
if (readArtifactSectionValue(reportContent, "Decision")?.trim().toLowerCase() !== "synced") {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
await validateDurableDocCommit(taskRepoRoot, assignment, reportContent, await deps.git.getHeadCommit(taskRepoRoot));
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
return errorMessage(error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
386
398
|
async function getCompletedDurableDocAssignment(taskRepoRoot, role, artifactPath) {
|
|
387
399
|
const runId = artifactPath.startsWith(`${MEMORY_REVIEW_RUNS_ROOT}/`)
|
|
388
400
|
? artifactPath.slice(MEMORY_REVIEW_RUNS_ROOT.length + 1).split("/")[0]
|
|
@@ -893,8 +905,9 @@ export function createAutoMemoryService(deps) {
|
|
|
893
905
|
async function validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead) {
|
|
894
906
|
const reportedCommit = readArtifactSectionContent(reportContent, "Commit");
|
|
895
907
|
let commit;
|
|
908
|
+
let subject;
|
|
896
909
|
try {
|
|
897
|
-
commit =
|
|
910
|
+
({ sha: commit, subject } = await deps.git.getCommitInfo(taskRepoRoot, reportedCommit));
|
|
898
911
|
}
|
|
899
912
|
catch (error) {
|
|
900
913
|
const detail = error instanceof VcmError && error.hint ? `${errorMessage(error)} ${error.hint}` : errorMessage(error);
|
|
@@ -919,6 +932,10 @@ export function createAutoMemoryService(deps) {
|
|
|
919
932
|
if (!changedPaths.includes(assignment.targetPath)) {
|
|
920
933
|
throw new Error(`Commit ${commit} modified ${assignment.targetPath}, but no net change remains in ${assignment.baseCommit}..${currentHead}. Verify the current document before reporting completion.`);
|
|
921
934
|
}
|
|
935
|
+
const subjectPrefix = durableDocCommitSubjectPrefix(assignment.id);
|
|
936
|
+
if (!subject.startsWith(subjectPrefix)) {
|
|
937
|
+
throw new Error(`Commit ${commit} subject must start with "${subjectPrefix}"; found "${subject}". Reword the commit (git commit --amend when it is the latest commit) and report it again.`);
|
|
938
|
+
}
|
|
922
939
|
return commit;
|
|
923
940
|
}
|
|
924
941
|
async function hashOptionalFile(taskRepoRoot, relativePath) {
|
|
@@ -1511,6 +1528,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1511
1528
|
isRoleMemoryTurn,
|
|
1512
1529
|
getDurableDocAssignment,
|
|
1513
1530
|
getCompletedDurableDocAssignment,
|
|
1531
|
+
checkDurableDocAssignmentReport,
|
|
1514
1532
|
handleRoleHook: (input) => withTaskLock(input.taskRepoRoot, () => handleRoleHook(input)),
|
|
1515
1533
|
handleHarnessEngineerHook: (input) => withTaskLock(input.taskRepoRoot, () => handleHarnessEngineerHook(input)),
|
|
1516
1534
|
assertHarnessEngineerAvailable
|
|
@@ -1619,6 +1637,7 @@ function buildDurableDocAssignmentPrompt(taskRepoRoot, assignment) {
|
|
|
1619
1637
|
...assignment.evidence.map((item) => `- ${item}`),
|
|
1620
1638
|
"",
|
|
1621
1639
|
"Verify the content, update the target and any directly related durable documentation, run applicable documentation checks, and commit the documentation changes.",
|
|
1640
|
+
`Use the commit subject "${durableDocCommitSubjectPrefix(assignment.id)} <summary>" so Project Manager can tell this post-acceptance documentation commit apart; VCM rejects a synced report whose commit has another subject.`,
|
|
1622
1641
|
`Assigned report path: ${assignment.reportPath}`,
|
|
1623
1642
|
`Submit with .ai/tools/vcm-artifact docs-update-report --file <candidate> --path ${assignment.reportPath} --mode final`,
|
|
1624
1643
|
"Set Assignment ID to the exact ID above.",
|
|
@@ -5,12 +5,21 @@ import { findPmUserQuestion } from "./pm-user-question.js";
|
|
|
5
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
6
6
|
import { matchesRoleHookSession } from "./session-service.js";
|
|
7
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
8
|
+
import { detectUsageLimit } from "./usage-limit.js";
|
|
8
9
|
const MAX_ROLE_RETRY_ATTEMPTS = 20;
|
|
9
10
|
const ROLE_RETRY_BASE_DELAY_MS = 60_000;
|
|
11
|
+
// A usage limit waits for its reset time, retried shortly after it; a limit
|
|
12
|
+
// whose reset time is unknown is checked again every hour. Waiting stops after
|
|
13
|
+
// eight days, longer than a weekly limit lasts.
|
|
14
|
+
const USAGE_LIMIT_RESET_BUFFER_MS = 2 * 60_000;
|
|
15
|
+
const USAGE_LIMIT_UNKNOWN_RESET_RETRY_MS = 60 * 60_000;
|
|
16
|
+
const MAX_USAGE_LIMIT_WAIT_MS = 8 * 24 * 60 * 60_000;
|
|
10
17
|
const NON_RETRYABLE_STOP_FAILURE_ERRORS = new Set([
|
|
11
18
|
"authentication_failed",
|
|
12
19
|
"oauth_org_not_allowed",
|
|
20
|
+
"account_on_hold",
|
|
13
21
|
"billing_error",
|
|
22
|
+
"cloud_credential_error",
|
|
14
23
|
"invalid_request",
|
|
15
24
|
"model_not_found",
|
|
16
25
|
"max_output_tokens",
|
|
@@ -464,7 +473,10 @@ export function createClaudeHookService(deps) {
|
|
|
464
473
|
}
|
|
465
474
|
});
|
|
466
475
|
}
|
|
467
|
-
const
|
|
476
|
+
const usageLimit = detectUsageLimit(failure, new Date(now()));
|
|
477
|
+
const retryResult = usageLimit
|
|
478
|
+
? await scheduleUsageLimitRetry(input, context, failure, usageLimit)
|
|
479
|
+
: await scheduleStopFailureRetry(input, context, failure);
|
|
468
480
|
if (retryResult === "scheduled") {
|
|
469
481
|
return {
|
|
470
482
|
ok: true,
|
|
@@ -586,6 +598,7 @@ export function createClaudeHookService(deps) {
|
|
|
586
598
|
role: input.role,
|
|
587
599
|
error: options.gatewayStopFailure.error,
|
|
588
600
|
errorDetails: options.gatewayStopFailure.errorDetails,
|
|
601
|
+
lastAssistantMessage: options.gatewayStopFailure.lastAssistantMessage,
|
|
589
602
|
attempt: options.gatewayStopFailure.attempt,
|
|
590
603
|
maxAttempts: options.gatewayStopFailure.maxAttempts
|
|
591
604
|
}).catch(() => undefined);
|
|
@@ -751,6 +764,76 @@ export function createClaudeHookService(deps) {
|
|
|
751
764
|
scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
|
|
752
765
|
return "scheduled";
|
|
753
766
|
}
|
|
767
|
+
// Waits for the usage limit to reset instead of retrying on the short-term
|
|
768
|
+
// schedule, which would give up after about three and a half hours while a
|
|
769
|
+
// session or weekly limit can last longer. Waits do not count as retry
|
|
770
|
+
// attempts, and Gateway hears about the limit once, when the wait starts.
|
|
771
|
+
async function scheduleUsageLimitRetry(input, context, failure, usageLimit) {
|
|
772
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
773
|
+
if (!preferences.roleRetryEnabled || !deps.runtime) {
|
|
774
|
+
return "not-scheduled";
|
|
775
|
+
}
|
|
776
|
+
const stateInput = createRoundStateInput(context);
|
|
777
|
+
const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
|
|
778
|
+
if (currentRoundState.stopReason === "manual-interrupt"
|
|
779
|
+
&& currentRoundState.status === "stopped"
|
|
780
|
+
&& currentRoundState.activeRole === input.role) {
|
|
781
|
+
return "manual-interrupt";
|
|
782
|
+
}
|
|
783
|
+
const previous = currentRoundState.roleRecovery?.role === input.role
|
|
784
|
+
&& currentRoundState.roleRecovery.status !== "failed"
|
|
785
|
+
? currentRoundState.roleRecovery
|
|
786
|
+
: undefined;
|
|
787
|
+
const alreadyWaiting = previous?.waitReason === "usage-limit";
|
|
788
|
+
const timestamp = now();
|
|
789
|
+
const nowMs = Date.parse(timestamp);
|
|
790
|
+
const limitSince = alreadyWaiting && previous?.limitSince ? previous.limitSince : timestamp;
|
|
791
|
+
const usageLimitFailure = { ...failure, usageLimit: { resetAt: usageLimit.resetAt, limitSince } };
|
|
792
|
+
if (nowMs - Date.parse(limitSince) >= MAX_USAGE_LIMIT_WAIT_MS) {
|
|
793
|
+
await markStopFailureRecoveryFailed(input, context, usageLimitFailure, previous?.attempt ?? 0, timestamp);
|
|
794
|
+
return "not-scheduled";
|
|
795
|
+
}
|
|
796
|
+
const resetMs = usageLimit.resetAt ? Date.parse(usageLimit.resetAt) : Number.NaN;
|
|
797
|
+
const nextRetryAt = new Date(resetMs > nowMs
|
|
798
|
+
? resetMs + USAGE_LIMIT_RESET_BUFFER_MS
|
|
799
|
+
: nowMs + USAGE_LIMIT_UNKNOWN_RESET_RETRY_MS).toISOString();
|
|
800
|
+
const attempt = previous?.attempt ?? 0;
|
|
801
|
+
await deps.roundService.setRoleRecovery({
|
|
802
|
+
...stateInput,
|
|
803
|
+
recovery: {
|
|
804
|
+
role: input.role,
|
|
805
|
+
status: "waiting",
|
|
806
|
+
attempt,
|
|
807
|
+
maxAttempts: MAX_ROLE_RETRY_ATTEMPTS,
|
|
808
|
+
lastFailureAt: timestamp,
|
|
809
|
+
error: failure.error,
|
|
810
|
+
errorDetails: failure.errorDetails,
|
|
811
|
+
lastAssistantMessage: failure.lastAssistantMessage,
|
|
812
|
+
retryable: true,
|
|
813
|
+
nextRetryAt,
|
|
814
|
+
waitReason: "usage-limit",
|
|
815
|
+
limitResetAt: usageLimit.resetAt,
|
|
816
|
+
limitSince
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
|
|
820
|
+
if (session) {
|
|
821
|
+
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role, session.id);
|
|
822
|
+
}
|
|
823
|
+
scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
|
|
824
|
+
if (!alreadyWaiting && (await isHookSessionBoundToTask(context, input.role))) {
|
|
825
|
+
void deps.gatewayService?.handleRoleStopFailure({
|
|
826
|
+
repoRoot: context.project.repoRoot,
|
|
827
|
+
taskSlug: context.taskSlug,
|
|
828
|
+
role: input.role,
|
|
829
|
+
error: failure.error,
|
|
830
|
+
errorDetails: failure.errorDetails,
|
|
831
|
+
lastAssistantMessage: failure.lastAssistantMessage,
|
|
832
|
+
usageLimitWait: { resetAt: usageLimit.resetAt, nextRetryAt }
|
|
833
|
+
}).catch(() => undefined);
|
|
834
|
+
}
|
|
835
|
+
return "scheduled";
|
|
836
|
+
}
|
|
754
837
|
async function isManualInterruptedStopFailure(context, role) {
|
|
755
838
|
try {
|
|
756
839
|
const state = await deps.roundService.getSessionRoundState(createRoundStateInput(context));
|
|
@@ -831,7 +914,10 @@ export function createClaudeHookService(deps) {
|
|
|
831
914
|
errorDetails: failure.errorDetails,
|
|
832
915
|
lastAssistantMessage: failure.lastAssistantMessage,
|
|
833
916
|
retryable: failure.retryable,
|
|
834
|
-
failedAt: timestamp
|
|
917
|
+
failedAt: timestamp,
|
|
918
|
+
...(failure.usageLimit
|
|
919
|
+
? { waitReason: "usage-limit", limitResetAt: failure.usageLimit.resetAt, limitSince: failure.usageLimit.limitSince }
|
|
920
|
+
: {})
|
|
835
921
|
}
|
|
836
922
|
});
|
|
837
923
|
}
|
|
@@ -127,7 +127,7 @@ export function createTranslationService(deps) {
|
|
|
127
127
|
taskFeeds.set(key, created);
|
|
128
128
|
return created;
|
|
129
129
|
}
|
|
130
|
-
async function prepareCache(input) {
|
|
130
|
+
async function prepareCache(input, options = {}) {
|
|
131
131
|
const state = getState(input.sessionId);
|
|
132
132
|
state.repoRoot = input.repoRoot;
|
|
133
133
|
state.baseRepoRoot = input.baseRepoRoot ?? input.repoRoot;
|
|
@@ -145,7 +145,9 @@ export function createTranslationService(deps) {
|
|
|
145
145
|
pruneTranslationEntries(input.sessionId);
|
|
146
146
|
}
|
|
147
147
|
syncTaskFeedFromSessionState(input.sessionId, state);
|
|
148
|
-
|
|
148
|
+
if (options.createDir !== false) {
|
|
149
|
+
await deps.fs.ensureDir(path.dirname(cachePath));
|
|
150
|
+
}
|
|
149
151
|
return state;
|
|
150
152
|
}
|
|
151
153
|
async function loadCachedEvents(state) {
|
|
@@ -1311,13 +1313,16 @@ export function createTranslationService(deps) {
|
|
|
1311
1313
|
};
|
|
1312
1314
|
const roleSession = await getGatewayRoleSession(input);
|
|
1313
1315
|
if (roleSession) {
|
|
1316
|
+
// A lookup only reads. A stopped session has no transcript tail to write
|
|
1317
|
+
// events, and its task may be closing: creating the cache directory there
|
|
1318
|
+
// would recreate the removed task worktree.
|
|
1314
1319
|
const state = await prepareCache({
|
|
1315
1320
|
repoRoot: roleSession.cwd,
|
|
1316
1321
|
baseRepoRoot: input.repoRoot,
|
|
1317
1322
|
taskSlug: input.taskSlug,
|
|
1318
1323
|
role: input.role,
|
|
1319
1324
|
sessionId: roleSession.id
|
|
1320
|
-
});
|
|
1325
|
+
}, { createDir: roleSession.status === "running" });
|
|
1321
1326
|
if (roleSession.status === "running") {
|
|
1322
1327
|
startTranscriptTail(roleSession);
|
|
1323
1328
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Claude Code reports a plan usage limit (the session or weekly limit) as an
|
|
2
|
+
// ordinary `rate_limit` API error, the same type as short-term throttling.
|
|
3
|
+
// Only the rendered message tells them apart, and it carries the reset time:
|
|
4
|
+
// "You've hit your session limit · resets 5:50pm (Asia/Singapore)"
|
|
5
|
+
// Retrying before that time cannot succeed, so a usage limit waits for the
|
|
6
|
+
// reset instead of following the short-term retry schedule.
|
|
7
|
+
// A reset further away than this is treated as unparsed; weekly limits reset
|
|
8
|
+
// within seven days.
|
|
9
|
+
export const MAX_USAGE_LIMIT_RESET_MS = 8 * 24 * 60 * 60 * 1000;
|
|
10
|
+
const USAGE_LIMIT_ERRORS = new Set(["rate_limit", "unknown"]);
|
|
11
|
+
const USAGE_LIMIT_PATTERNS = [
|
|
12
|
+
/\bhit your\b[^\n]*?\blimit\b/i,
|
|
13
|
+
/\busage limit\b/i,
|
|
14
|
+
/\b(?:session|weekly|daily|monthly)\s+limit\b/i
|
|
15
|
+
];
|
|
16
|
+
const RESET_PATTERN = /\bresets?\s+(?:at\s+|on\s+)?([^()\n]+?)\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)+|UTC|GMT)\)/i;
|
|
17
|
+
const TIME_PATTERN = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*$/i;
|
|
18
|
+
const DATE_PATTERN = /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+(\d{1,2})\b/i;
|
|
19
|
+
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
20
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
export function detectUsageLimit(failure, now) {
|
|
22
|
+
if (!USAGE_LIMIT_ERRORS.has(failure.error)) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const message = [failure.lastAssistantMessage, failure.errorDetails]
|
|
26
|
+
.find((text) => Boolean(text) && USAGE_LIMIT_PATTERNS.some((pattern) => pattern.test(text)));
|
|
27
|
+
if (!message) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return { message, resetAt: parseUsageLimitReset(message, now) };
|
|
31
|
+
}
|
|
32
|
+
// Parses "resets 5:50pm (Asia/Singapore)" or "resets Nov 12, 9am
|
|
33
|
+
// (America/Los_Angeles)" into the next matching instant after `now`.
|
|
34
|
+
export function parseUsageLimitReset(message, now) {
|
|
35
|
+
const match = RESET_PATTERN.exec(message);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const when = match[1].trim();
|
|
40
|
+
const timeZone = match[2];
|
|
41
|
+
if (!isTimeZone(timeZone)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const time = TIME_PATTERN.exec(when);
|
|
45
|
+
if (!time) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
let hour = Number(time[1]);
|
|
49
|
+
const minute = Number(time[2] ?? 0);
|
|
50
|
+
const meridiem = time[3]?.toLowerCase();
|
|
51
|
+
if (minute > 59) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (meridiem) {
|
|
55
|
+
if (hour < 1 || hour > 12) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
hour = (hour % 12) + (meridiem === "pm" ? 12 : 0);
|
|
59
|
+
}
|
|
60
|
+
else if (hour > 23) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const today = zonedDate(now, timeZone);
|
|
64
|
+
const date = DATE_PATTERN.exec(when);
|
|
65
|
+
let resetMs;
|
|
66
|
+
if (date) {
|
|
67
|
+
const month = MONTHS.indexOf(date[1].toLowerCase().slice(0, 3)) + 1;
|
|
68
|
+
const day = Number(date[2]);
|
|
69
|
+
const explicitYear = /\b(20\d{2})\b/.exec(when)?.[1];
|
|
70
|
+
const year = explicitYear ? Number(explicitYear) : today.year;
|
|
71
|
+
resetMs = zonedTimeToUtc(year, month, day, hour, minute, timeZone);
|
|
72
|
+
if (!explicitYear && resetMs < now.getTime() - DAY_MS) {
|
|
73
|
+
resetMs = zonedTimeToUtc(year + 1, month, day, hour, minute, timeZone);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
resetMs = zonedTimeToUtc(today.year, today.month, today.day, hour, minute, timeZone);
|
|
78
|
+
if (resetMs <= now.getTime()) {
|
|
79
|
+
resetMs = zonedTimeToUtc(today.year, today.month, today.day + 1, hour, minute, timeZone);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isFinite(resetMs) || resetMs - now.getTime() > MAX_USAGE_LIMIT_RESET_MS) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return new Date(resetMs).toISOString();
|
|
86
|
+
}
|
|
87
|
+
function isTimeZone(timeZone) {
|
|
88
|
+
try {
|
|
89
|
+
new Intl.DateTimeFormat("en-US", { timeZone });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function zonedParts(instantMs, timeZone) {
|
|
97
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
98
|
+
timeZone,
|
|
99
|
+
hourCycle: "h23",
|
|
100
|
+
year: "numeric",
|
|
101
|
+
month: "2-digit",
|
|
102
|
+
day: "2-digit",
|
|
103
|
+
hour: "2-digit",
|
|
104
|
+
minute: "2-digit",
|
|
105
|
+
second: "2-digit"
|
|
106
|
+
}).formatToParts(new Date(instantMs));
|
|
107
|
+
const value = (type) => Number(parts.find((part) => part.type === type)?.value);
|
|
108
|
+
return {
|
|
109
|
+
year: value("year"),
|
|
110
|
+
month: value("month"),
|
|
111
|
+
day: value("day"),
|
|
112
|
+
hour: value("hour"),
|
|
113
|
+
minute: value("minute"),
|
|
114
|
+
second: value("second")
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function zonedDate(now, timeZone) {
|
|
118
|
+
const parts = zonedParts(now.getTime(), timeZone);
|
|
119
|
+
return { year: parts.year, month: parts.month, day: parts.day };
|
|
120
|
+
}
|
|
121
|
+
// The time zone's offset from UTC at an instant, in milliseconds.
|
|
122
|
+
function offsetMs(instantMs, timeZone) {
|
|
123
|
+
const parts = zonedParts(instantMs, timeZone);
|
|
124
|
+
const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
125
|
+
return asUtc - Math.floor(instantMs / 1000) * 1000;
|
|
126
|
+
}
|
|
127
|
+
// The UTC instant of a wall-clock time in a time zone; the second pass
|
|
128
|
+
// corrects for an offset change (daylight saving) between the two guesses.
|
|
129
|
+
function zonedTimeToUtc(year, month, day, hour, minute, timeZone) {
|
|
130
|
+
const wallClockAsUtc = Date.UTC(year, month - 1, day, hour, minute);
|
|
131
|
+
const first = wallClockAsUtc - offsetMs(wallClockAsUtc, timeZone);
|
|
132
|
+
return wallClockAsUtc - offsetMs(first, timeZone);
|
|
133
|
+
}
|
|
@@ -820,7 +820,9 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
|
|
|
820
820
|
if (source === "architect-debug" && artifact.disposition === ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION) {
|
|
821
821
|
return ["code-change/architect"];
|
|
822
822
|
}
|
|
823
|
-
|
|
823
|
+
// Only a completed analysis-only Diagnosis returns to its parent; a pending
|
|
824
|
+
// or blocked one sends the same Architect back.
|
|
825
|
+
if (source === "architecture-diagnosis" && artifact.complete && artifact.disposition === "analysis completed") {
|
|
824
826
|
return parentFlow ? [`${parentFlow}/architect`] : [];
|
|
825
827
|
}
|
|
826
828
|
if (artifact.disposition === "user clarification required") {
|
|
@@ -954,7 +956,7 @@ async function artifactState(fs, input, fileName, kind) {
|
|
|
954
956
|
if (kind === "architect-debug")
|
|
955
957
|
value = inline("Status") ?? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
|
|
956
958
|
if (kind === "architecture-diagnosis")
|
|
957
|
-
value = readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
|
|
959
|
+
value = inline("Status") ?? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
|
|
958
960
|
if (kind === "test-report")
|
|
959
961
|
value = inline("Test Result");
|
|
960
962
|
if (kind === "docs-update-report" || kind === "final-acceptance") {
|
|
@@ -1190,7 +1192,10 @@ async function validateCompletion(fs, input, state, candidate) {
|
|
|
1190
1192
|
const inBranch = flowRun.activeBranch === candidate.flow;
|
|
1191
1193
|
if (candidate.flow === "architecture-diagnosis" && !inBranch) {
|
|
1192
1194
|
const diagnosis = await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
|
|
1193
|
-
|
|
1195
|
+
// The final contract rejects analysis completed with a commit or changed
|
|
1196
|
+
// files, so a completed analysis-only Diagnosis changed no code.
|
|
1197
|
+
if (diagnosis.complete
|
|
1198
|
+
&& diagnosis.disposition === "analysis completed"
|
|
1194
1199
|
&& evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "architecture-diagnosis.md", diagnosis.hash))
|
|
1195
1200
|
return;
|
|
1196
1201
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_RULE, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
1
|
+
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_DIAGNOSIS_STATUSES, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_RULE, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
2
2
|
const CURRENT_HANDOFF_NOTICE = "<!-- VCM current handoff: replace this file with one complete, self-contained snapshot of the current result. Restate all still-relevant evidence; do not refer to a prior revision, route message, Session, or transcript as evidence. -->";
|
|
3
3
|
export function renderArchitectureBriefTemplate(taskSlug) {
|
|
4
4
|
return `# Architecture Brief: ${taskSlug}
|
|
@@ -417,6 +417,8 @@ export function renderArchitectureDiagnosisTemplate(taskSlug) {
|
|
|
417
417
|
|
|
418
418
|
${CURRENT_HANDOFF_NOTICE}
|
|
419
419
|
|
|
420
|
+
Status: ${renderArtifactOptions(ARCHITECTURE_DIAGNOSIS_STATUSES)}
|
|
421
|
+
|
|
420
422
|
## Diagnosis Boundary
|
|
421
423
|
|
|
422
424
|
TBD
|
|
@@ -286,9 +286,11 @@ Each rewritten \`architecture-diagnosis.md\` must be a complete, self-contained
|
|
|
286
286
|
- Before reporting \`diagnosis implementation completed\`, run every existing L2/L3 check applicable to the diagnosed failure path.
|
|
287
287
|
- Every applicable L2/L3 check must pass. If a level is not applicable, record the concrete reason.
|
|
288
288
|
- If an applicable L2/L3 check is unavailable or cannot complete, do not report \`diagnosis implementation completed\`; report the exact blocker.
|
|
289
|
+
- Keep \`Status: pending\` while the Diagnosis is in progress or blocked, and record the exact blocker under \`Implementation And Validation\`. Set \`Status: completed\` only with a final disposition.
|
|
289
290
|
- Under \`Implementation And Validation\`, record each L2/L3 command or test case, the diagnosed failure path it covers, its result, and its evidence.
|
|
290
291
|
- Architect-run Diagnosis validation is implementation evidence and does not replace Tester final validation.
|
|
291
292
|
- Final disposition must be one of: \`analysis completed\`, \`diagnosis implementation completed\`, or \`user clarification required\`.
|
|
293
|
+
- \`analysis completed\` means the Diagnosis changed no code: \`Changed Files And Public Surface\` and \`Commit\` are exactly \`None.\`. A Diagnosis that changed or committed code reports \`diagnosis implementation completed\`, or stays \`Status: pending\` while blocked.
|
|
292
294
|
|
|
293
295
|
### Replan And Drift
|
|
294
296
|
|
|
@@ -66,6 +66,7 @@ If a reusable harness problem is suspected, it is enough to record a concise fee
|
|
|
66
66
|
- Communication-Only Flow uses: \`project-manager response or relay -> completion\`.
|
|
67
67
|
- Gate Review is PM-triggered at its defined trigger points; the tool decides whether review is enabled or required.
|
|
68
68
|
- Ignore commits whose subject starts with \`[VCM Harness]\`. Do not review, attribute, or route those commits unless they cause a test failure. A test failure caused by a Harness commit follows the normal test-failure flow.
|
|
69
|
+
- Commits whose subject starts with \`[VCM Docs]\` are durable-documentation updates that VCM assigned from a memory review after Final Acceptance. They belong to the task and its PR; do not review, re-gate, or route them.
|
|
69
70
|
- Final acceptance closes only a complete code-delivery flow. After a Debug or Diagnosis Branch passes its code-diff Gate, final acceptance closes the parent flow it returned to, never the branch alone.
|
|
70
71
|
- PR-Preparation Flow starts only after the active delivery flow completes; every complete code-delivery flow requires final acceptance to pass.
|
|
71
72
|
- If Docs-Only Flow or Validation-Only Flow reveals that the accepted outcome requires production-code, runtime-behavior, public-contract, dependency, or system-architecture changes, project-manager routes through the full Code-Change Flow.
|
|
@@ -401,6 +401,7 @@ If required work or evidence is incomplete, return to the responsible flow or ro
|
|
|
401
401
|
PR-Preparation Flow does not perform technical review, validation, documentation updates, Gate Review, or Final Acceptance.
|
|
402
402
|
|
|
403
403
|
- Create a draft PR by default unless the user requests a ready PR.
|
|
404
|
+
- Commits after the HEAD the latest code-diff Gate approved: \`[VCM Harness]\` and \`[VCM Docs]\` commits belong to the PR without another Gate. List each \`[VCM Docs]\` commit with its assignment ID (the subject's \`[VCM Docs] <assignment-id>:\`) in the PR description. Any other commit after that HEAD is unreviewed task work: return it to the responsible flow before preparing the PR.
|
|
404
405
|
|
|
405
406
|
### Background Jobs
|
|
406
407
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_PATTERN, DOCS_UPDATE_COMMIT_RULE, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
|
|
1
|
+
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_ANALYSIS_ONLY_DISPOSITION, ARCHITECTURE_DIAGNOSIS_ANALYSIS_ONLY_SECTIONS, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_DIAGNOSIS_STATUSES, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_PATTERN, DOCS_UPDATE_COMMIT_RULE, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
|
|
2
2
|
import { getArtifactDefinition } from "./artifact-registry.js";
|
|
3
3
|
const PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|Not run yet\.?|status:\s*draft)\s*(\n|$)/i;
|
|
4
4
|
export function checkMarkdownArtifact(kind, artifactPath, content, options = {}) {
|
|
@@ -104,10 +104,24 @@ function validateArtifactFields(kind, content, mode) {
|
|
|
104
104
|
return invalidFields;
|
|
105
105
|
}
|
|
106
106
|
if (kind === "architecture-diagnosis") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
// Like architect-debug, an in-progress or blocked Diagnosis stays
|
|
108
|
+
// `Status: pending`; only a completed one carries a Final Disposition.
|
|
109
|
+
const invalidFields = validateLifecycleField(content, "Status", ARCHITECTURE_DIAGNOSIS_STATUSES, "completed", mode);
|
|
110
|
+
if (mode === "final") {
|
|
111
|
+
const disposition = readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
|
|
112
|
+
if (!isAllowedValue(disposition, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS)) {
|
|
113
|
+
invalidFields.push(renderExactSectionError("Final Disposition", ARCHITECTURE_DIAGNOSIS_DISPOSITIONS.join("|"), disposition));
|
|
114
|
+
}
|
|
115
|
+
else if (disposition === ARCHITECTURE_DIAGNOSIS_ANALYSIS_ONLY_DISPOSITION) {
|
|
116
|
+
for (const section of ARCHITECTURE_DIAGNOSIS_ANALYSIS_ONLY_SECTIONS) {
|
|
117
|
+
const value = readArtifactSectionContent(content, section)?.trim();
|
|
118
|
+
if (value !== STRICT_NONE_VALUE) {
|
|
119
|
+
invalidFields.push(`${section} must be exactly "${STRICT_NONE_VALUE}" for analysis completed; a Diagnosis that changed or committed code reports diagnosis implementation completed, or stays Status: pending while blocked.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return invalidFields;
|
|
111
125
|
}
|
|
112
126
|
if (kind === "test-report") {
|
|
113
127
|
const result = readInlineField(content, "Test Result");
|
|
@@ -15,6 +15,11 @@ export const ARCHITECT_DEBUG_DISPOSITIONS = [
|
|
|
15
15
|
ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION,
|
|
16
16
|
"user clarification required"
|
|
17
17
|
];
|
|
18
|
+
export const ARCHITECTURE_DIAGNOSIS_STATUSES = ["pending", "completed"];
|
|
19
|
+
export const ARCHITECTURE_DIAGNOSIS_ANALYSIS_ONLY_DISPOSITION = "analysis completed";
|
|
20
|
+
// An analysis-only Diagnosis changes no code: these sections must be exactly
|
|
21
|
+
// "None." for its final disposition.
|
|
22
|
+
export const ARCHITECTURE_DIAGNOSIS_ANALYSIS_ONLY_SECTIONS = ["Changed Files And Public Surface", "Commit"];
|
|
18
23
|
export const ARCHITECTURE_DIAGNOSIS_DISPOSITIONS = [
|
|
19
24
|
"analysis completed",
|
|
20
25
|
"diagnosis implementation completed",
|
|
@@ -31,6 +36,12 @@ export const L3_REQUIRED_VALUES = ["yes", "no"];
|
|
|
31
36
|
export const L3_ACTIONS = ["run-existing", "updated", "added"];
|
|
32
37
|
export const DOCS_REPORT_DECISIONS = ["synced", "unchanged", "blocked"];
|
|
33
38
|
export const DOCS_UPDATE_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/;
|
|
39
|
+
// Subject prefix of the commits a durable-documentation assignment makes, so
|
|
40
|
+
// Project Manager can tell these post-acceptance documentation commits apart.
|
|
41
|
+
export const DURABLE_DOC_COMMIT_PREFIX = "[VCM Docs]";
|
|
42
|
+
export function durableDocCommitSubjectPrefix(assignmentId) {
|
|
43
|
+
return `${DURABLE_DOC_COMMIT_PREFIX} ${assignmentId}:`;
|
|
44
|
+
}
|
|
34
45
|
export const DOCS_UPDATE_COMMIT_RULE = "When Decision is synced, Commit must contain only one bare lowercase hexadecimal commit hash (7-40 characters, for example abc1234) on a single line. No backticks, bullets, links, subjects, or extra lines; put explanations in Evidence Reviewed. An unchanged result does not require a new commit.";
|
|
35
46
|
export const FINAL_ACCEPTANCE_DECISIONS = [
|
|
36
47
|
"accepted",
|