@khoavu882/doflow 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/bin/doflow.js +1 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/settings/settings.json +8 -9
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
- package/core/harnesses/copilot/hooks/doflow.json +0 -6
- package/core/harnesses/kiro/agents/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
- package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
- package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
- package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
- package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
- package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +1 -1
- package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
- package/core/registry/contracts.json +1 -1
- package/core/registry/contribution-points.json +5 -2
- package/core/registry/gates.json +5 -5
- package/core/registry/harnesses.json +9 -9
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +133 -711
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/packages/doflow/doflow-package.json +2 -2
- package/core/registry/rules.json +8 -8
- package/core/registry/settings.json +0 -4
- package/core/registry/storage.json +8 -0
- package/core/registry/telemetry.json +1 -1
- package/core/registry/verification.json +0 -5
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
- package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +2 -2
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
- package/core/shared/scripts/doflow/bin/doflow-run +5 -2
- package/core/shared/skills/do/SKILL.md +3 -3
- package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
- package/core/shared/skills/do-code-review/SKILL.md +7 -4
- package/core/shared/skills/do-design/SKILL.md +5 -3
- package/core/shared/skills/do-diagnose/SKILL.md +4 -2
- package/core/shared/skills/do-document/SKILL.md +1 -0
- package/core/shared/skills/do-document/references/embedded-research.md +1 -1
- package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
- package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
- package/core/shared/skills/do-flow/SKILL.md +4 -4
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-implement/SKILL.md +9 -7
- package/core/shared/skills/do-plan/SKILL.md +9 -6
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/package.json +1 -1
- package/src/adapters/claude/index.js +23 -2
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +13 -0
- package/src/adapters/copy-tree.js +1 -2
- package/src/cli/commands/status.js +1 -1
- package/src/cli/index.js +20 -8
- package/src/cli/runtime-commands.js +12 -4
- package/src/lifecycle/coverage.js +1 -30
- package/src/lifecycle/hook-tier.js +2 -2
- package/src/release/docs-blocks.js +32 -52
- package/src/runtime/context/pointer.js +67 -12
- package/src/runtime/context-pack.js +4 -1
- package/src/runtime/evidence-ledger.js +18 -0
- package/src/runtime/health.js +59 -1
- package/src/runtime/implementation-gate.js +4 -4
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/lifecycle/cli.js +73 -5
- package/src/runtime/lifecycle/followup.js +32 -6
- package/src/runtime/lifecycle/overview.js +2 -1
- package/src/runtime/packages/points.js +10 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/gates.js +6 -3
- package/src/runtime/roadmap/loop.js +113 -30
- package/src/runtime/roadmap/merge.js +17 -8
- package/src/runtime/roadmap/rows.js +13 -4
- package/src/runtime/roadmap/seed.js +153 -41
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/settings/registry.js +1 -1
- package/src/runtime/slots.js +1 -1
- package/src/runtime/task-classifier.js +39 -4
- package/src/runtime/task-state.js +2 -3
- package/src/runtime/verb-view.js +267 -0
- package/src/runtime/verification/engine.js +0 -45
- package/src/runtime/worker/launch.js +5 -37
- package/src/runtime/worker/readiness.js +2 -2
- package/src/runtime/workflow-orchestrator.js +33 -5
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
|
@@ -13,8 +13,7 @@ function safeText(value ) {
|
|
|
13
13
|
if (typeof value === 'string') {
|
|
14
14
|
try {
|
|
15
15
|
text = JSON.stringify(value).slice(1, -1)
|
|
16
|
-
.replace(
|
|
17
|
-
.replace(/\u2029/g, '\\u2029');
|
|
16
|
+
.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, (c ) => { const cp = c.codePointAt(0) ; return cp > 0xffff ? `\\u{${cp.toString(16)}}` : `\\u${cp.toString(16).padStart(4, '0')}`; });
|
|
18
17
|
} catch {
|
|
19
18
|
text = '';
|
|
20
19
|
}
|
|
@@ -733,13 +732,22 @@ function deriveStatus(
|
|
|
733
732
|
return { statuses, rowStatus };
|
|
734
733
|
}
|
|
735
734
|
|
|
735
|
+
/**
|
|
736
|
+
* The task classes the roadmap loop runs: the feature chain, and the four classes that take a
|
|
737
|
+
* gate-b-only policy (core/registry/gates.json). research, review and operations stay out: they
|
|
738
|
+
* produce no change the loop could merge. dependency-change stays out too: a change to any
|
|
739
|
+
* package.json dependency key is always denied `protected-path:package.json` (gates.ts), so such a
|
|
740
|
+
* row could never merge without a person; a user ruling reopens it.
|
|
741
|
+
*/
|
|
742
|
+
const LOOP_CLASSES = Object.freeze(['feature', 'bug', 'refactor', 'documentation', 'trivial-edit']);
|
|
743
|
+
|
|
736
744
|
/**
|
|
737
745
|
* S1 Pick table: chooses the next ready row in seed order.
|
|
738
746
|
* - Ready only when all needs are merged
|
|
739
747
|
* - Write set disjoint from all in-flight rows
|
|
740
748
|
* - WIP bound (default 2)
|
|
741
749
|
* - Row without a write set runs alone
|
|
742
|
-
* -
|
|
750
|
+
* - A class outside LOOP_CLASSES (research, review, operations, dependency-change, anything else) is class-unsupported
|
|
743
751
|
* - Yields one reason per skipped row
|
|
744
752
|
*/
|
|
745
753
|
function pick(
|
|
@@ -782,7 +790,7 @@ function pick(
|
|
|
782
790
|
}
|
|
783
791
|
|
|
784
792
|
// Class check
|
|
785
|
-
if (row.class
|
|
793
|
+
if (!LOOP_CLASSES.includes(row.class)) {
|
|
786
794
|
skips[row.id] = 'class-unsupported';
|
|
787
795
|
continue;
|
|
788
796
|
}
|
|
@@ -1107,6 +1115,7 @@ module.exports = {
|
|
|
1107
1115
|
check,
|
|
1108
1116
|
seedFindings,
|
|
1109
1117
|
registryClasses,
|
|
1118
|
+
LOOP_CLASSES,
|
|
1110
1119
|
SEED_RULES,
|
|
1111
1120
|
ROW_KEYS,
|
|
1112
1121
|
TOP_KEYS,
|
|
@@ -54,6 +54,11 @@ const { readSeedSwitch } = require('../settings/autopilot.js');
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
function judgeDraft({
|
|
@@ -379,7 +384,7 @@ function printTextResult(
|
|
|
379
384
|
if ((op === 'apply' || op === 'amend') && result.ok) {
|
|
380
385
|
line1 += `, ${result.writes} files written`;
|
|
381
386
|
if (Array.isArray(result.proposed) && result.proposed.length > 0) {
|
|
382
|
-
line1 += `, ${result.proposed.length} proposed (no person verified, or autopilot is not verifiably off; a person runs in their terminal
|
|
387
|
+
line1 += `, ${result.proposed.length} proposed (no person verified, or autopilot is not verifiably off; a person runs in their terminal: doflow roadmap promote ${result.proposed.map(safeText).join(' ')})`;
|
|
383
388
|
}
|
|
384
389
|
}
|
|
385
390
|
if (result.seed && result.seed.sha256 === null) {
|
|
@@ -481,6 +486,9 @@ function handleSeedCommand(options = {}) {
|
|
|
481
486
|
err = (s ) => console.error(s),
|
|
482
487
|
classes,
|
|
483
488
|
isTTY = process.stdin.isTTY === true,
|
|
489
|
+
confirm,
|
|
490
|
+
ttyClock,
|
|
491
|
+
ttySleep,
|
|
484
492
|
} = options;
|
|
485
493
|
|
|
486
494
|
const usage = (message ) => {
|
|
@@ -583,13 +591,37 @@ function handleSeedCommand(options = {}) {
|
|
|
583
591
|
}
|
|
584
592
|
|
|
585
593
|
// 4. Apply / Amend action
|
|
586
|
-
// (a) maint-thin: a seed write is allowed whatever the switch says. Rows land as drafted (ready) only when
|
|
587
|
-
// autopilot is verifiably off (the fail-closed seed reader)
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
//
|
|
594
|
+
// (a) maint-thin / AU C05: a seed write is allowed whatever the switch says. Rows land as drafted (ready) only when
|
|
595
|
+
// stdin is an interactive terminal AND either autopilot is verifiably off (the fail-closed seed reader) or a person
|
|
596
|
+
// answers yes to the same `/dev/tty` [y/N] that `roadmap promote` uses. A model with a shell controls the
|
|
597
|
+
// environment (an empty config home reads as off), so the switch alone cannot tell a person; and without the prompt
|
|
598
|
+
// a model with a pty could land ready rows while autopilot is on, which widens the queue. No terminal, or no/other
|
|
599
|
+
// answer, lands every row it adds or amends `proposed`, which autopilot never picks or advances until a person runs
|
|
600
|
+
// `roadmap promote`. The prompt comes before the roadmap lease (Ctrl-C would otherwise leave the lease behind) and
|
|
601
|
+
// only for a draft that judges clean against the store as read now. No flag or environment variable skips it.
|
|
591
602
|
const switchReader = readSwitch || readSeedSwitch;
|
|
592
|
-
|
|
603
|
+
let landProposed = isTTY !== true || switchReader(env, fsImpl).enabled !== false;
|
|
604
|
+
if (landProposed && isTTY === true) {
|
|
605
|
+
const preFacts = gitFacts || gatherGitFacts({ root });
|
|
606
|
+
const pre = preFacts.unreadable && preFacts.unreadable.length > 0 ? null
|
|
607
|
+
: judgeDraft({ draftRaw, seedRaw, gitFacts: preFacts, op: action, classes });
|
|
608
|
+
if (pre && pre.ok) {
|
|
609
|
+
const rowsList = (JSON.parse(draftRaw).rows || []).filter((r ) => r && typeof r === 'object');
|
|
610
|
+
const ids = rowsList.map((r ) => safeText(r.id));
|
|
611
|
+
const titles = rowsList.map((r ) => ` ${safeText(r.id)} ${safeText(r.title)}`);
|
|
612
|
+
const prompt = `${titles.join('\n')}\nLand ${ids.length} row(s) ready for autopilot (autopilot is not verifiably off): ${ids.join(' ')}? [y/N] `;
|
|
613
|
+
let answer = null;
|
|
614
|
+
try {
|
|
615
|
+
if (confirm) answer = confirm(prompt);
|
|
616
|
+
else {
|
|
617
|
+
const read = readTtyLine(prompt, { timeoutMs: SEED_PROMPT_TIMEOUT_MS, now: ttyClock, sleep: ttySleep });
|
|
618
|
+
answer = read.answer;
|
|
619
|
+
if (read.timedOut) err(`doflow roadmap seed: the [y/N] prompt timed out after ${Math.round(SEED_PROMPT_TIMEOUT_MS / 1000)} s with no answer; rows land proposed (run: doflow roadmap promote <id...>)`);
|
|
620
|
+
}
|
|
621
|
+
} catch { answer = null; }
|
|
622
|
+
if (typeof answer === 'string' && /^(y|yes)$/i.test(answer.trim())) landProposed = false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
593
625
|
|
|
594
626
|
// (b) Acquire lease
|
|
595
627
|
const leaseDir = path.join(root, '.doflow', 'state', 'roadmap');
|
|
@@ -818,25 +850,97 @@ function handleSeedCommand(options = {}) {
|
|
|
818
850
|
|
|
819
851
|
|
|
820
852
|
|
|
821
|
-
|
|
822
853
|
|
|
823
|
-
|
|
824
854
|
|
|
825
855
|
|
|
826
856
|
|
|
827
857
|
|
|
828
858
|
|
|
829
859
|
|
|
860
|
+
|
|
861
|
+
|
|
830
862
|
|
|
831
863
|
|
|
864
|
+
/** How long the seed prompt waits for a person before it counts as "no" (promote waits without limit). */
|
|
865
|
+
const SEED_PROMPT_TIMEOUT_MS = 60000;
|
|
866
|
+
const TTY_POLL_MS = 25;
|
|
867
|
+
|
|
868
|
+
function sleepMs(ms ) {
|
|
869
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Reads one answer line from the controlling terminal (`/dev/tty`), never from stdin, an argument or the environment.
|
|
881
|
+
* `answer` is null when the terminal cannot be opened or read, the line is too long, or the optional timeout passes
|
|
882
|
+
* (`timedOut`). A timed read opens the terminal non-blocking and polls, so a pty that stays open without an answer
|
|
883
|
+
* cannot hold the caller for ever.
|
|
884
|
+
*/
|
|
885
|
+
function readTtyLine(prompt , options = {}) {
|
|
886
|
+
const { timeoutMs = 0, now = Date.now, sleep = sleepMs } = options;
|
|
887
|
+
const timed = timeoutMs > 0;
|
|
888
|
+
let fd ;
|
|
889
|
+
try { fd = timed ? fs.openSync('/dev/tty', fs.constants.O_RDWR | fs.constants.O_NONBLOCK) : fs.openSync('/dev/tty', 'r+'); } catch { return { answer: null, timedOut: false }; }
|
|
890
|
+
const deadline = timed ? now() + timeoutMs : Infinity;
|
|
891
|
+
let timedOut = false;
|
|
892
|
+
// one byte; 0 at end of input, -1 when the timeout passed
|
|
893
|
+
const readByte = (buf ) => {
|
|
894
|
+
for (;;) {
|
|
895
|
+
try { return fs.readSync(fd, buf, 0, 1, null); }
|
|
896
|
+
catch (error ) {
|
|
897
|
+
if (!timed || (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK')) throw error;
|
|
898
|
+
if (now() >= deadline) { timedOut = true; return -1; }
|
|
899
|
+
sleep(TTY_POLL_MS);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
try {
|
|
904
|
+
fs.writeSync(fd, prompt);
|
|
905
|
+
const buf = Buffer.alloc(1);
|
|
906
|
+
let line = '';
|
|
907
|
+
let ended = false;
|
|
908
|
+
for (let i = 0; i < 64 && !ended; i += 1) {
|
|
909
|
+
const n = readByte(buf);
|
|
910
|
+
if (n < 0) return { answer: null, timedOut: true };
|
|
911
|
+
if (n === 0 || buf[0] === 10) ended = true;
|
|
912
|
+
else line += String.fromCharCode(buf[0]);
|
|
913
|
+
}
|
|
914
|
+
// drain what is left of the typed line (bounded), so it is not left in the terminal queue
|
|
915
|
+
const tooLong = !ended;
|
|
916
|
+
for (let i = 0; i < 4096 && !ended; i += 1) {
|
|
917
|
+
const n = readByte(buf);
|
|
918
|
+
if (n < 0) return { answer: null, timedOut: true };
|
|
919
|
+
if (n === 0 || buf[0] === 10) ended = true;
|
|
920
|
+
}
|
|
921
|
+
return { answer: tooLong ? null : line, timedOut: false };
|
|
922
|
+
} catch {
|
|
923
|
+
return { answer: null, timedOut };
|
|
924
|
+
} finally {
|
|
925
|
+
try { fs.closeSync(fd); } catch { /* ignore close failure */ }
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** The untimed reader `roadmap promote` uses: a person runs that verb and may take as long as they need. */
|
|
930
|
+
function readTtyAnswer(prompt ) {
|
|
931
|
+
return readTtyLine(prompt).answer;
|
|
932
|
+
}
|
|
933
|
+
|
|
832
934
|
/**
|
|
833
|
-
* maint-thin: `roadmap promote <id...>`, person only. A model with a shell controls the environment, so an
|
|
834
|
-
* environment check
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* `needs-person` and
|
|
838
|
-
*
|
|
839
|
-
*
|
|
935
|
+
* maint-thin: `roadmap promote <id...>`, person only. A model with a shell controls the environment, so neither an
|
|
936
|
+
* environment check nor the autopilot switch can tell a person. The verb requires an interactive terminal on stdin
|
|
937
|
+
* (refused `needs-person-tty`, nothing written), validates the named rows, then prints their ids and titles and asks
|
|
938
|
+
* `[y/N]` on the controlling terminal (`/dev/tty`): no `/dev/tty`, or an answer other than y/yes, is refused
|
|
939
|
+
* `needs-person-confirm` and nothing is written. No flag or environment variable skips the prompt, and the autopilot
|
|
940
|
+
* switch is not read. The prompt comes before the roadmap lease (Ctrl-C would otherwise leave the lease behind); under
|
|
941
|
+
* the lease the store is read again and must equal what was shown, then the verb drops the `proposed` mark of every
|
|
942
|
+
* named row, so autopilot may pick it. All or nothing: an unknown id, a row that is not proposed, or a duplicate id is a finding (reported before any
|
|
943
|
+
* prompt) and nothing is written. Exit 0 promoted, 1 refused or findings, 2 usage.
|
|
840
944
|
*/
|
|
841
945
|
function handlePromoteCommand(options = {}) {
|
|
842
946
|
const {
|
|
@@ -845,15 +949,14 @@ function handlePromoteCommand(options = {})
|
|
|
845
949
|
root = resolveMainRoot(process.cwd()),
|
|
846
950
|
fsImpl = fs,
|
|
847
951
|
gitFacts,
|
|
848
|
-
readSwitch,
|
|
849
952
|
alive,
|
|
850
|
-
env = process.env,
|
|
851
953
|
pid,
|
|
852
954
|
host,
|
|
853
955
|
now,
|
|
854
956
|
out = (s ) => console.log(s),
|
|
855
957
|
err = (s ) => console.error(s),
|
|
856
958
|
isTTY = process.stdin.isTTY === true,
|
|
959
|
+
confirm = readTtyAnswer,
|
|
857
960
|
} = options;
|
|
858
961
|
const emit = (result , code , detail ) => {
|
|
859
962
|
if (json) out(JSON.stringify(result));
|
|
@@ -879,13 +982,32 @@ function handlePromoteCommand(options = {})
|
|
|
879
982
|
return emit({ ...base, reason: 'needs-person-tty' }, 1,
|
|
880
983
|
'promote runs only from an interactive terminal: a person runs doflow roadmap promote in their own shell');
|
|
881
984
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
985
|
+
|
|
986
|
+
// Read, validate and ask BEFORE taking the lease: Ctrl-C kills the process inside the terminal read, so a lease held
|
|
987
|
+
// across the prompt would be left behind. The lease is taken after the answer, and the store is read again under it.
|
|
988
|
+
let raw ;
|
|
989
|
+
try { raw = fsImpl.readFileSync(storePath, 'utf8'); }
|
|
990
|
+
catch (error ) { return emit({ ...base, findings: ids.map((id) => ({ rule: 'row-not-found', rowId: id, message: `no store at ${storePath}: ${safeText(error.message)}` })) }, 1); }
|
|
991
|
+
const storeFindings = seedFindings(raw);
|
|
992
|
+
if (storeFindings.length > 0) return emit({ ...base, findings: storeFindings }, 1);
|
|
993
|
+
const doc = JSON.parse(raw);
|
|
994
|
+
const findings = [];
|
|
995
|
+
const seen = new Set ();
|
|
996
|
+
for (const id of ids) {
|
|
997
|
+
const row = doc.rows.find((r ) => r.id === id);
|
|
998
|
+
if (seen.has(id)) findings.push({ rule: 'duplicate-id', rowId: id, message: `Row '${safeText(id)}' is named twice` });
|
|
999
|
+
else if (!row) findings.push({ rule: 'row-not-found', rowId: id, message: `Row '${safeText(id)}' not found in store` });
|
|
1000
|
+
else if (row.proposed !== true) findings.push({ rule: 'row-not-proposed', rowId: id, message: `Row '${safeText(id)}' is not proposed` });
|
|
1001
|
+
seen.add(id);
|
|
1002
|
+
}
|
|
1003
|
+
if (findings.length > 0) return emit({ ...base, findings }, 1);
|
|
1004
|
+
const titles = ids.map((id) => ` ${safeText(id)} ${safeText(doc.rows.find((r ) => r.id === id).title)}`);
|
|
1005
|
+
const prompt = `${titles.join('\n')}\nPromote ${ids.length} row(s): ${ids.map(safeText).join(' ')}? [y/N] `;
|
|
1006
|
+
let answer = null;
|
|
1007
|
+
try { answer = confirm(prompt); } catch { answer = null; }
|
|
1008
|
+
if (typeof answer !== 'string' || !/^(y|yes)$/i.test(answer.trim())) {
|
|
1009
|
+
return emit({ ...base, reason: 'needs-person-confirm' }, 1,
|
|
1010
|
+
'promote needs a person to answer y on the controlling terminal (/dev/tty); there is no flag or variable that skips the prompt');
|
|
889
1011
|
}
|
|
890
1012
|
|
|
891
1013
|
const leaseDir = path.join(root, '.doflow', 'state', 'roadmap');
|
|
@@ -901,22 +1023,12 @@ function handlePromoteCommand(options = {})
|
|
|
901
1023
|
const lease = acquireLease(leaseDir, me, alive || noPsAlive, fsImpl);
|
|
902
1024
|
if (!lease.ok) return emit({ ...base, reason: 'lease-held' }, 1);
|
|
903
1025
|
try {
|
|
904
|
-
let
|
|
905
|
-
try {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const findings = [];
|
|
911
|
-
const seen = new Set ();
|
|
912
|
-
for (const id of ids) {
|
|
913
|
-
const row = doc.rows.find((r ) => r.id === id);
|
|
914
|
-
if (seen.has(id)) findings.push({ rule: 'duplicate-id', rowId: id, message: `Row '${safeText(id)}' is named twice` });
|
|
915
|
-
else if (!row) findings.push({ rule: 'row-not-found', rowId: id, message: `Row '${safeText(id)}' not found in store` });
|
|
916
|
-
else if (row.proposed !== true) findings.push({ rule: 'row-not-proposed', rowId: id, message: `Row '${safeText(id)}' is not proposed` });
|
|
917
|
-
seen.add(id);
|
|
918
|
-
}
|
|
919
|
-
if (findings.length > 0) return emit({ ...base, findings }, 1);
|
|
1026
|
+
let current = null;
|
|
1027
|
+
try { current = fsImpl.readFileSync(storePath, 'utf8'); } catch { current = null; }
|
|
1028
|
+
if (current !== raw) {
|
|
1029
|
+
return emit({ ...base, reason: 'needs-person-confirm' }, 1,
|
|
1030
|
+
'the roadmap store changed after the rows were shown; nothing was written, run doflow roadmap promote again');
|
|
1031
|
+
}
|
|
920
1032
|
const rows = doc.rows.map((r ) => {
|
|
921
1033
|
if (!ids.includes(r.id)) return r;
|
|
922
1034
|
const { proposed, ...ready } = r;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `doflow-run worktree --action cleanup --slug <slug>` (autonomy plan section 7c): remove one merged
|
|
5
|
+
* feature's worktree and branch with merge.ts's non-force `cleanupFeature`, after copying the
|
|
6
|
+
* worktree's gitignored `agent-docs/doflow/<slug>` back to the main checkout. It refuses, with the
|
|
7
|
+
* reason printed and exit 1, whenever `cleanupFeature` keeps the feature (`report.kept`), the roadmap
|
|
8
|
+
* lease is held (`lease-held`), the row has no `DoFlow-Row` trailer merge on develop (`not-merged`, the
|
|
9
|
+
* evidence the loop requires; a tip merely contained in develop is not enough) or the caller stands
|
|
10
|
+
* inside the worktree (`cwd-inside-worktree`). There is no `--force` (G53). This module is not part of the
|
|
11
|
+
* person-only set and must not import attendance.ts (attendance-scope guard).
|
|
12
|
+
*
|
|
13
|
+
* Exit: 0 `cleaned`, 1 `kept` or `denied` (reason in the output), 2 usage.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { execFileSync: defaultExecFileSync } = require('node:child_process');
|
|
20
|
+
const { acquireLease, releaseLease, resolveMainRoot, noPsAlive } = require('./loop.js');
|
|
21
|
+
const { cleanupFeature } = require('./merge.js');
|
|
22
|
+
const { parseSeed, deriveStatus } = require('./rows.js');
|
|
23
|
+
const { gatherGitFacts } = require('./facts.js');
|
|
24
|
+
|
|
25
|
+
/** A row slug is `<id>-<words>`; the pattern keeps a path separator and `..` out of every join below. */
|
|
26
|
+
const SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
27
|
+
const ALLOWED_FLAGS = new Set(['--action', '--slug', '--json']);
|
|
28
|
+
|
|
29
|
+
function execOptionsFor(root , env ) {
|
|
30
|
+
const environment = { ...env };
|
|
31
|
+
for (const key of Object.keys(environment)) if (/^GIT_(?:DIR|WORK_TREE|COMMON_DIR|INDEX_FILE|OBJECT_DIRECTORY|ALTERNATE_OBJECT_DIRECTORIES|CONFIG_COUNT|CONFIG_KEY_\d+|CONFIG_VALUE_\d+)$/.test(key)) delete environment[key];
|
|
32
|
+
return { cwd: root, encoding: 'utf8', env: environment, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Branch ref to worktree path, as `git worktree list` reports them. */
|
|
36
|
+
function listWorktrees(root , execFileSync , execOptions ) {
|
|
37
|
+
const out = String(execFileSync('git', ['-C', root, 'worktree', 'list', '--porcelain', '-z'], execOptions));
|
|
38
|
+
const map = new Map ();
|
|
39
|
+
for (const block of out.split('\0\0').filter(Boolean)) {
|
|
40
|
+
const fields = block.split('\0');
|
|
41
|
+
const listed = fields.find((s) => s.startsWith('worktree '))?.slice(9);
|
|
42
|
+
const worktree = listed === undefined ? listed : path.resolve(listed);
|
|
43
|
+
const branch = fields.find((s) => s.startsWith('branch '))?.slice(7);
|
|
44
|
+
if (worktree && branch) map.set(branch, worktree);
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** True when the seed row for `slug` has a `DoFlow-Row` trailer merge on develop (deriveStatus, as the loop reads it). */
|
|
50
|
+
function mergeEvidence({ root, slug, fsImpl } ) {
|
|
51
|
+
let rows ;
|
|
52
|
+
try { rows = parseSeed(fsImpl.readFileSync(path.join(root, 'agent-docs', 'roadmap', 'rows.json'), 'utf8')).rows; }
|
|
53
|
+
catch { return { error: 'facts-unreadable', unreadable: ['seed'] }; }
|
|
54
|
+
const row = rows.find((entry ) => entry && entry.slug === slug);
|
|
55
|
+
if (!row) return { error: 'row-unknown' };
|
|
56
|
+
const facts = gatherGitFacts({ root });
|
|
57
|
+
if (facts.unreadable?.length) return { error: 'facts-unreadable', unreadable: facts.unreadable };
|
|
58
|
+
const info = deriveStatus([row], facts).statuses.get(row.id);
|
|
59
|
+
return { merged: info?.status === 'merged' && info.evidence?.kind === 'trailer' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when `cwd` is the worktree or inside it (real paths, so a symlinked temp folder cannot hide it). */
|
|
63
|
+
function callerInside(worktree , cwd , fsImpl ) {
|
|
64
|
+
if (!worktree) return false;
|
|
65
|
+
const real = (value ) => { try { return fsImpl.realpathSync(value); } catch { return path.resolve(value); } };
|
|
66
|
+
const relative = path.relative(real(worktree), real(cwd));
|
|
67
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @returns {{code: number, result: object}} the exit code and the JSON body; never throws for a refusal.
|
|
72
|
+
*/
|
|
73
|
+
function cleanupWorktree(options = {}) {
|
|
74
|
+
const fsImpl = options.fsImpl || fs;
|
|
75
|
+
const env = options.env || process.env;
|
|
76
|
+
const execFileSync = options.execFileSync || defaultExecFileSync;
|
|
77
|
+
const slug = options.slug;
|
|
78
|
+
const body = (outcome , extra = {}) => ({ schema: 1, action: 'cleanup', outcome, slug, ...extra });
|
|
79
|
+
if (typeof slug !== 'string' || !SLUG.test(slug) || slug.includes('..')) {
|
|
80
|
+
return { code: 2, result: { schema: 1, action: 'cleanup', error: 'usage', detail: '--slug <row slug> is required (letters, digits, dot, underscore, hyphen)' } };
|
|
81
|
+
}
|
|
82
|
+
const root = options.root || resolveMainRoot(options.cwd || process.cwd());
|
|
83
|
+
const execOptions = execOptionsFor(root, env);
|
|
84
|
+
const dir = path.join(root, '.doflow', 'state', 'roadmap');
|
|
85
|
+
const me = { schema: 1, pid: options.pid ?? process.pid, start: new Date(Date.now() - (process ).uptime() * 1000).toISOString(),
|
|
86
|
+
host: options.host ?? os.hostname(), runId: null, acquiredAt: new Date().toISOString() };
|
|
87
|
+
const lease = acquireLease(dir, me, options.alive || noPsAlive, fsImpl);
|
|
88
|
+
if (!lease.ok) {
|
|
89
|
+
const holder = lease.holder && typeof lease.holder === 'object' ? { pid: lease.holder.pid ?? null, host: lease.holder.host ?? null, runId: lease.holder.runId ?? null } : null;
|
|
90
|
+
return { code: 1, result: body('denied', { reason: 'lease-held', holder }) };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
let worktrees ;
|
|
94
|
+
try { worktrees = listWorktrees(root, execFileSync, execOptions); }
|
|
95
|
+
catch { return { code: 1, result: body('denied', { reason: 'facts-unreadable', rule: 'cleanup/facts', unreadable: ['worktrees'] }) }; }
|
|
96
|
+
const develop = worktrees.get('refs/heads/develop');
|
|
97
|
+
if (!develop) return { code: 1, result: body('denied', { reason: 'no-develop-worktree' }) };
|
|
98
|
+
const worktree = worktrees.get(`refs/heads/feat/${slug}`) || null;
|
|
99
|
+
let featureHead = null;
|
|
100
|
+
try { featureHead = String(execFileSync('git', ['-C', root, 'rev-parse', '--verify', `refs/heads/feat/${slug}`], execOptions)).trim(); }
|
|
101
|
+
catch { /* branch already gone: only a leftover worktree can remain */ }
|
|
102
|
+
if (featureHead === null && worktree === null) return { code: 0, result: body('cleaned', { already: true, artifacts: { copied: 0, skipped: [], conflicts: [] } }) };
|
|
103
|
+
const artifacts = {};
|
|
104
|
+
// The loop cleans a row only on trailer evidence (loop.ts, merged rows with no intent): a branch
|
|
105
|
+
// whose tip merely sits inside develop (a row claimed a moment ago) is not a merged feature.
|
|
106
|
+
const evidence = mergeEvidence({ root, slug, fsImpl });
|
|
107
|
+
if (evidence.error) return { code: 1, result: body('denied', { reason: evidence.error, rule: 'cleanup/merge-evidence', unreadable: evidence.unreadable }) };
|
|
108
|
+
if (!evidence.merged) { artifacts.kept = 'not-merged'; return { code: 1, result: body('kept', { reason: artifacts.kept, artifacts }) }; }
|
|
109
|
+
// Removing the directory the caller stands in would strand its session.
|
|
110
|
+
if (callerInside(worktree, options.cwd ?? process.cwd(), fsImpl)) {
|
|
111
|
+
artifacts.kept = 'cwd-inside-worktree';
|
|
112
|
+
return { code: 1, result: body('kept', { reason: artifacts.kept, artifacts }) };
|
|
113
|
+
}
|
|
114
|
+
const cleaned = cleanupFeature({ intent: { featureWorktree: worktree, featureBranch: `feat/${slug}`, featureHead },
|
|
115
|
+
root, develop, execFileSync, execOptions, fsImpl, report: artifacts });
|
|
116
|
+
if (cleaned) return { code: 0, result: body('cleaned', { artifacts }) };
|
|
117
|
+
return { code: 1, result: body('kept', { reason: artifacts.kept || 'git-failed', artifacts }) };
|
|
118
|
+
} finally {
|
|
119
|
+
releaseLease(dir, me, fsImpl);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The CLI handler: `--action cleanup --slug <slug> [--json]`; any other action is usage. */
|
|
124
|
+
function handleWorktreeCommand(options = {}) {
|
|
125
|
+
const json = Boolean(options.json);
|
|
126
|
+
const usage = (detail ) => {
|
|
127
|
+
if (json) console.log(JSON.stringify({ schema: 1, error: 'usage', detail }));
|
|
128
|
+
else console.error(`doflow worktree: error: ${detail}`);
|
|
129
|
+
process.exitCode = 2;
|
|
130
|
+
return 2;
|
|
131
|
+
};
|
|
132
|
+
if (options.action !== 'cleanup') return usage('--action cleanup is the only action');
|
|
133
|
+
const extra = ((options.flags || []) ).map((flag) => flag.split('=')[0]).find((flag) => !ALLOWED_FLAGS.has(flag));
|
|
134
|
+
if (extra !== undefined) return usage(`unsupported flag ${extra}; only --action, --slug and --json apply`);
|
|
135
|
+
const { code, result } = cleanupWorktree({ slug: options.slug, cwd: options.cwd, env: options.env, root: options.root });
|
|
136
|
+
if (result.error === 'usage') return usage(result.detail);
|
|
137
|
+
if (json) console.log(JSON.stringify(result));
|
|
138
|
+
else if (result.outcome === 'cleaned') console.log(`cleaned ${result.slug}${result.already ? ' (already gone)' : ''}`);
|
|
139
|
+
else {
|
|
140
|
+
const failure = result.artifacts?.failure ? ` (${result.artifacts.failure})` : '';
|
|
141
|
+
console.log(`${result.outcome} ${result.slug}: ${result.reason}${failure}`);
|
|
142
|
+
}
|
|
143
|
+
process.exitCode = code;
|
|
144
|
+
return code;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { cleanupWorktree, handleWorktreeCommand };
|
|
@@ -294,7 +294,7 @@ function validateSettingsRegistry(raw , refs = { meta: REF_MET
|
|
|
294
294
|
const safe = spec.failSafe;
|
|
295
295
|
// Records and worker lists use the consumer's invalid path as their fail-safe (IC-003).
|
|
296
296
|
// A project-only key's candidates never widen (resolve.ts canWiden), so no fail-safe value is ever
|
|
297
|
-
// taken; the consumer's invalid path voids it
|
|
297
|
+
// taken; the consumer's invalid path voids it, so the state form is allowed.
|
|
298
298
|
const stateSafe = (keyed || workerList || spec.scope === 'project-only') && isObject(safe) && safe.state === 'invalid';
|
|
299
299
|
if (!stateSafe && (!has(spec, 'failSafe') || (isObject(safe) && !has(safe, 'gate')))) err('is securityRelevant and needs a failSafe value or {gate}');
|
|
300
300
|
}
|
package/src/runtime/slots.js
CHANGED
|
@@ -185,7 +185,7 @@ const refuse = (why ) => Object.assign(new Error(why), { code: 'EBUSY' })
|
|
|
185
185
|
* directory name this lock does not make once the lock is 10 s old, as in task-state. An older
|
|
186
186
|
* runtime's lock (its `owner.json` token) is judged the same way. `reclaimed` records that this acquire
|
|
187
187
|
* judged a lock reclaimable, so it holds only after `quiet` (every reclaim quiets; user ruling, 117
|
|
188
|
-
* decisions: amends 115 C-4's "dead owner reclaimed at once").
|
|
188
|
+
* decisions: amends 115 C-4's "dead owner reclaimed at once").
|
|
189
189
|
*/
|
|
190
190
|
function slotLock(fsImpl , deps ) {
|
|
191
191
|
const start = deps.procStart(deps.pid);
|
|
@@ -6,7 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const { TextDecoder } = require('node:util');
|
|
8
8
|
const { WorkflowEngine, CALLER_ROLES } = require('./workflow-engine.js');
|
|
9
|
-
const { finishRuntime } = require('./cli-result.js');
|
|
9
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
10
|
+
const { ViewUsageError, resolveView, projectBrief, viewLines, withEnvelope, emitJson } = require('./verb-view.js');
|
|
10
11
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
11
12
|
const { loadAdviceRegistry } = require('./advice/registry.js');
|
|
12
13
|
const { createAdviceClient } = require('./advice/client.js');
|
|
@@ -421,6 +422,25 @@ class TaskClassifier {
|
|
|
421
422
|
}
|
|
422
423
|
}
|
|
423
424
|
|
|
425
|
+
/**
|
|
426
|
+
* The brief view of a classify answer (feature 137, IC-005): the fields the skills' classify call sites read.
|
|
427
|
+
* Everything else is dropped; `--view full` returns the whole object.
|
|
428
|
+
*/
|
|
429
|
+
const CLASSIFY_BRIEF = Object.freeze({
|
|
430
|
+
verb: 'classify',
|
|
431
|
+
carry: Object.freeze(['taskClass', 'callingSkill']),
|
|
432
|
+
keep: Object.freeze([
|
|
433
|
+
'$.outcome', '$.taskClass', '$.proposedClass', '$.callingSkill', '$.reason', '$.message', '$.validClasses',
|
|
434
|
+
'$.suggestions', '$.callerSuggestions', '$.fit.state', '$.fit.reason', '$.fit.hostingClasses', '$.secondOpinion',
|
|
435
|
+
'$.workflow.taskClass', '$.workflow.handoff', '$.workflow.stageIds', '$.workflow.hasImplementationStage',
|
|
436
|
+
'$.workflow.requiresImplementationReadiness', '$.workflow.stages[].id', '$.workflow.stages[].skill',
|
|
437
|
+
'$.workflow.stages[].agent', '$.workflow.stages[].kind', '$.workflow.stages[].optional',
|
|
438
|
+
'$.workflow.stages[].readinessTemplate', '$.workflow.stages[].mutatesSource', '$.workflow.gates[].id',
|
|
439
|
+
'$.workflow.gates[].afterStage', '$.workflow.gates[].kind', '$.workflow.gates[].trigger',
|
|
440
|
+
'$.workflow.gates[].prompt',
|
|
441
|
+
]),
|
|
442
|
+
});
|
|
443
|
+
|
|
424
444
|
/**
|
|
425
445
|
* Handles `doflow classify` — validate a proposed task class against the workflow registry.
|
|
426
446
|
*
|
|
@@ -437,16 +457,31 @@ class TaskClassifier {
|
|
|
437
457
|
* @param {string} [options.request] request text for a second opinion
|
|
438
458
|
* @param {string} [options.requestFile] file containing request text for a second opinion
|
|
439
459
|
* @param {boolean} [options.json=false]
|
|
460
|
+
* @param {string} [options.view] `brief` or `full` (default): the JSON answer's view; text output is the same in both
|
|
440
461
|
* @returns {number|Promise<number>} exit code; advisory calls are asynchronous
|
|
441
462
|
*/
|
|
442
|
-
function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill, request, requestFile, taskId, projectRoot, env = process.env, json = false } = {}) {
|
|
463
|
+
function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill, request, requestFile, taskId, projectRoot, view, env = process.env, json = false } = {}) {
|
|
464
|
+
let resolvedView;
|
|
465
|
+
try {
|
|
466
|
+
resolvedView = resolveView(view, 'full');
|
|
467
|
+
} catch (error ) {
|
|
468
|
+
if (error instanceof ViewUsageError) return usageError('classify', error.message, json);
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
const printJson = (opinion ) => {
|
|
472
|
+
const answer = { ...decision, secondOpinion: opinion };
|
|
473
|
+
if (resolvedView === 'full') return emitJson(answer);
|
|
474
|
+
return emitJson(withEnvelope(projectBrief(answer, CLASSIFY_BRIEF.keep), {
|
|
475
|
+
view: 'brief', next: viewLines('full', { spec: CLASSIFY_BRIEF, carried: { taskClass, callingSkill }, json: true }),
|
|
476
|
+
}));
|
|
477
|
+
};
|
|
443
478
|
const classifier = new TaskClassifier({ repoRoot: REPO_ROOT });
|
|
444
479
|
const decision = classifier.classify({ taskClass, rationale, proposedBy, callingSkill });
|
|
445
480
|
const exitCode = decision.outcome === CLASSIFICATION_OUTCOMES.ACCEPTED
|
|
446
481
|
? 0 : decision.reason === REJECTION_REASONS.MISSING_CLASS ? 2 : 1;
|
|
447
482
|
const hasRequest = request !== undefined || requestFile !== undefined;
|
|
448
483
|
if (!hasRequest) {
|
|
449
|
-
if (json)
|
|
484
|
+
if (json) printJson(null);
|
|
450
485
|
else printClassifyDecision(decision, null);
|
|
451
486
|
return finishRuntime(exitCode);
|
|
452
487
|
}
|
|
@@ -528,7 +563,7 @@ function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill,
|
|
|
528
563
|
opinion = emptySecondOpinion('unavailable', 'config-invalid');
|
|
529
564
|
}
|
|
530
565
|
}
|
|
531
|
-
if (json)
|
|
566
|
+
if (json) printJson(opinion);
|
|
532
567
|
else printClassifyDecision(decision, opinion);
|
|
533
568
|
return finishRuntime(exitCode);
|
|
534
569
|
}
|
|
@@ -103,8 +103,7 @@ function sleep(ms ) {
|
|
|
103
103
|
// Runtimes from before this protocol see a directory they did not create: they wait on a fresh one,
|
|
104
104
|
// and on a stale one their rmdir fails (it is not empty), so they fail closed ("Could not lock",
|
|
105
105
|
// `store-locked`) instead of taking it; their release of a lock reclaimed from them fails the same
|
|
106
|
-
// way. Whatever a process leaves when it dies is reclaimed once LOCK_STALE_MS old.
|
|
107
|
-
// this does not cover) are listed once, in docs/architecture.md under "The task-state lock".
|
|
106
|
+
// way. Whatever a process leaves when it dies is reclaimed once LOCK_STALE_MS old.
|
|
108
107
|
const OWNER_PREFIX = 'owner-';
|
|
109
108
|
// The longest a fresh acquire may take from its mkdir to its entry and still trust the directory is
|
|
110
109
|
// its own: half the stale age, so a process that judged it stale cannot have done so before the
|
|
@@ -307,7 +306,7 @@ function ownedRelease(fsImpl , lockDir , me ) {
|
|
|
307
306
|
try { fsImpl.rmdirSync(entry); } catch { return; } // reclaimed: the lock is not ours to remove
|
|
308
307
|
// Removed only if it is still that directory: a release stalled here past LOCK_STALE_MS may find
|
|
309
308
|
// an older runtime's new (empty) lock at the path. What stays is the gap between this stat and the
|
|
310
|
-
// rmdir
|
|
309
|
+
// rmdir.
|
|
311
310
|
try {
|
|
312
311
|
if (mine && sameDir(mine, fsImpl.statSync(lockDir))) fsImpl.rmdirSync(lockDir);
|
|
313
312
|
} catch { /* a waiting process's entry is in it; that process holds next */ }
|