@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
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/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -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 +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* repository itself uses (for test stubs, which have no file extension of their own to read).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const { detectCommands } = require('../command-detect.
|
|
13
|
+
const { detectCommands } = require('../command-detect.js');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Per-file language comes from the file's own extension, because `plan.md` already states it:
|
|
@@ -22,7 +22,7 @@ const { detectCommands } = require('../command-detect.ts');
|
|
|
22
22
|
* is this repository" — is `command-detect.ts`'s, so it is asked there rather than answered a
|
|
23
23
|
* second time here. See `repoLanguage()` for the one place its output is interpreted.
|
|
24
24
|
*/
|
|
25
|
-
const EXTENSION_LANGUAGE
|
|
25
|
+
const EXTENSION_LANGUAGE = Object.freeze({
|
|
26
26
|
'.js': 'javascript',
|
|
27
27
|
'.mjs': 'javascript',
|
|
28
28
|
'.cjs': 'javascript',
|
|
@@ -41,7 +41,7 @@ const EXTENSION_LANGUAGE: Readonly<Record<string, string>> = Object.freeze({
|
|
|
41
41
|
* generator — there is no signature to emit for a markdown file — so these skip benignly and never
|
|
42
42
|
* push the run to a non-zero exit.
|
|
43
43
|
*/
|
|
44
|
-
const PROSE_EXTENSIONS
|
|
44
|
+
const PROSE_EXTENSIONS = new Set([
|
|
45
45
|
'.md', '.markdown', '.txt', '.rst',
|
|
46
46
|
'.yaml', '.yml', '.json', '.jsonc', '.toml', '.ini', '.cfg', '.conf', '.env', '.lock',
|
|
47
47
|
'.html', '.css', '.scss', '.svg', '.png', '.jpg', '.gif', '.csv',
|
|
@@ -53,7 +53,7 @@ const PROSE_EXTENSIONS: ReadonlySet<string> = new Set([
|
|
|
53
53
|
* plan that touches one produces "no emitter for Java", not silence — the gap is real and belongs
|
|
54
54
|
* in the manifest, not in a reader's assumptions.
|
|
55
55
|
*/
|
|
56
|
-
const UNEMITTED_SOURCE_EXTENSIONS
|
|
56
|
+
const UNEMITTED_SOURCE_EXTENSIONS = Object.freeze({
|
|
57
57
|
'.java': { name: 'Java', inProseGrammar: true },
|
|
58
58
|
'.kt': { name: 'Kotlin', inProseGrammar: true },
|
|
59
59
|
'.kts': { name: 'Kotlin', inProseGrammar: true },
|
|
@@ -73,7 +73,7 @@ const UNEMITTED_SOURCE_EXTENSIONS: Readonly<Record<string, { name: string; inPro
|
|
|
73
73
|
|
|
74
74
|
/** Splits an identifier-ish path segment into lowercase words, so every language can re-case it
|
|
75
75
|
* to its own convention from one parse. */
|
|
76
|
-
function words(name
|
|
76
|
+
function words(name ) {
|
|
77
77
|
return name
|
|
78
78
|
.replace(/\.[^.]+$/, '')
|
|
79
79
|
.split(/[^A-Za-z0-9]+/)
|
|
@@ -82,9 +82,9 @@ function words(name: string): string[] {
|
|
|
82
82
|
.map((part) => part.toLowerCase());
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const camel = (parts
|
|
86
|
-
const pascal = (parts
|
|
87
|
-
const snake = (parts
|
|
85
|
+
const camel = (parts ) => parts.map((w, i) => (i === 0 ? w : w[0].toUpperCase() + w.slice(1))).join('') || 'scaffold';
|
|
86
|
+
const pascal = (parts ) => parts.map((w) => w[0].toUpperCase() + w.slice(1)).join('') || 'Scaffold';
|
|
87
|
+
const snake = (parts ) => parts.join('_') || 'scaffold';
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* One entry per language this module can emit. Each `module` and `test` body is a signature plus a
|
|
@@ -92,10 +92,10 @@ const snake = (parts: string[]): string => parts.join('_') || 'scaffold';
|
|
|
92
92
|
* `references/scaffold.md` — the same table the external-dependency case uses, so the two halves of
|
|
93
93
|
* that document agree on what an unimplemented body looks like.
|
|
94
94
|
*/
|
|
95
|
-
const EMITTERS
|
|
95
|
+
const EMITTERS = Object.freeze({
|
|
96
96
|
javascript: {
|
|
97
|
-
line: (t
|
|
98
|
-
module: ({ symbol, message }
|
|
97
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
98
|
+
module: ({ symbol, message } ) => [
|
|
99
99
|
'/**',
|
|
100
100
|
' * Placeholder signature — replace with the real surface before implementing.',
|
|
101
101
|
' * @returns {never}',
|
|
@@ -106,11 +106,11 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
106
106
|
'',
|
|
107
107
|
`module.exports = { ${symbol} };`,
|
|
108
108
|
].join('\n'),
|
|
109
|
-
testName: (id
|
|
110
|
-
test: ({ id, cases, message }
|
|
109
|
+
testName: (id ) => `${id}.test.js`,
|
|
110
|
+
test: ({ id, cases, message } ) => [
|
|
111
111
|
"const { test } = require('node:test');",
|
|
112
112
|
'',
|
|
113
|
-
...cases.map(({ statement }
|
|
113
|
+
...cases.map(({ statement } ) => [
|
|
114
114
|
`test(${JSON.stringify(`${id} — ${statement}`)}, () => {`,
|
|
115
115
|
` throw new Error(${JSON.stringify(message)});`,
|
|
116
116
|
'});',
|
|
@@ -120,18 +120,18 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
120
120
|
},
|
|
121
121
|
|
|
122
122
|
typescript: {
|
|
123
|
-
line: (t
|
|
124
|
-
module: ({ symbol, message }
|
|
123
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
124
|
+
module: ({ symbol, message } ) => [
|
|
125
125
|
'/** Placeholder signature — replace with the real surface before implementing. */',
|
|
126
126
|
`export function ${symbol}(): never {`,
|
|
127
127
|
` throw new Error(${JSON.stringify(message)});`,
|
|
128
128
|
'}',
|
|
129
129
|
].join('\n'),
|
|
130
|
-
testName: (id
|
|
131
|
-
test: ({ id, cases, message }
|
|
130
|
+
testName: (id ) => `${id}.test.ts`,
|
|
131
|
+
test: ({ id, cases, message } ) => [
|
|
132
132
|
"import { test } from 'node:test';",
|
|
133
133
|
'',
|
|
134
|
-
...cases.map(({ statement }
|
|
134
|
+
...cases.map(({ statement } ) => [
|
|
135
135
|
`test(${JSON.stringify(`${id} — ${statement}`)}, (): void => {`,
|
|
136
136
|
` throw new Error(${JSON.stringify(message)});`,
|
|
137
137
|
'});',
|
|
@@ -141,14 +141,14 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
141
141
|
},
|
|
142
142
|
|
|
143
143
|
python: {
|
|
144
|
-
line: (t
|
|
145
|
-
module: ({ symbol, message }
|
|
144
|
+
line: (t ) => (t ? `# ${t}` : '#'),
|
|
145
|
+
module: ({ symbol, message } ) => [
|
|
146
146
|
`def ${symbol}() -> None:`,
|
|
147
147
|
' """Placeholder signature — replace with the real surface before implementing."""',
|
|
148
148
|
` raise NotImplementedError(${JSON.stringify(message)})`,
|
|
149
149
|
].join('\n'),
|
|
150
|
-
testName: (id
|
|
151
|
-
test: ({ cases, message }
|
|
150
|
+
testName: (id ) => `test_${id.toLowerCase().replace(/-/g, '_')}.py`,
|
|
151
|
+
test: ({ cases, message } ) => cases.map(({ statement } , i ) => [
|
|
152
152
|
`def test_case_${i + 1}() -> None:`,
|
|
153
153
|
` """${statement.replace(/"/g, "'")}"""`,
|
|
154
154
|
` raise NotImplementedError(${JSON.stringify(message)})`,
|
|
@@ -157,8 +157,8 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
157
157
|
},
|
|
158
158
|
|
|
159
159
|
go: {
|
|
160
|
-
line: (t
|
|
161
|
-
module: ({ symbol, message, pkg }
|
|
160
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
161
|
+
module: ({ symbol, message, pkg } ) => [
|
|
162
162
|
`package ${pkg}`,
|
|
163
163
|
'',
|
|
164
164
|
'import "errors"',
|
|
@@ -168,13 +168,13 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
168
168
|
`\treturn errors.New(${JSON.stringify(message)})`,
|
|
169
169
|
'}',
|
|
170
170
|
].join('\n'),
|
|
171
|
-
testName: (id
|
|
172
|
-
test: ({ id, cases, message, pkg }
|
|
171
|
+
testName: (id ) => `${id.toLowerCase().replace(/-/g, '_')}_test.go`,
|
|
172
|
+
test: ({ id, cases, message, pkg } ) => [
|
|
173
173
|
`package ${pkg}`,
|
|
174
174
|
'',
|
|
175
175
|
'import "testing"',
|
|
176
176
|
'',
|
|
177
|
-
...cases.map(({ statement }
|
|
177
|
+
...cases.map(({ statement } , i ) => [
|
|
178
178
|
`// ${statement}`,
|
|
179
179
|
`func Test${pascal(words(id))}Case${i + 1}(t *testing.T) {`,
|
|
180
180
|
`\tt.Fatal(${JSON.stringify(message)})`,
|
|
@@ -185,15 +185,15 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
185
185
|
},
|
|
186
186
|
|
|
187
187
|
rust: {
|
|
188
|
-
line: (t
|
|
189
|
-
module: ({ symbol, message }
|
|
188
|
+
line: (t ) => (t ? `// ${t}` : '//'),
|
|
189
|
+
module: ({ symbol, message } ) => [
|
|
190
190
|
'/// Placeholder signature — replace with the real surface before implementing.',
|
|
191
191
|
`pub fn ${symbol}() {`,
|
|
192
192
|
` unimplemented!(${JSON.stringify(message)});`,
|
|
193
193
|
'}',
|
|
194
194
|
].join('\n'),
|
|
195
|
-
testName: (id
|
|
196
|
-
test: ({ id, cases, message }
|
|
195
|
+
testName: (id ) => `${id.toLowerCase().replace(/-/g, '_')}.rs`,
|
|
196
|
+
test: ({ id, cases, message } ) => cases.map(({ statement } , i ) => [
|
|
197
197
|
`// ${statement}`,
|
|
198
198
|
'#[test]',
|
|
199
199
|
`fn ${snake(words(id))}_case_${i + 1}() {`,
|
|
@@ -212,12 +212,12 @@ const EMITTERS: Readonly<Record<string, any>> = Object.freeze({
|
|
|
212
212
|
* TypeScript. The one extra bit is read from its own output (a `typecheck` role exists only when a
|
|
213
213
|
* tsc script or a `tsconfig.json` was found), so no second detector is introduced.
|
|
214
214
|
*/
|
|
215
|
-
function repoLanguage(repoRoot
|
|
215
|
+
function repoLanguage(repoRoot , fsImpl ) {
|
|
216
216
|
const detected = detectCommands({ projectRoot: repoRoot, fsImpl });
|
|
217
217
|
if (!detected.manifestFound) {
|
|
218
218
|
return { id: null, signal: 'none', why: 'no build manifest found by command-detect.ts' };
|
|
219
219
|
}
|
|
220
|
-
const has = (name
|
|
220
|
+
const has = (name ) => detected.manifests.includes(name);
|
|
221
221
|
if (has('package.json')) {
|
|
222
222
|
const typescript = Boolean(detected.commands.typecheck);
|
|
223
223
|
return { id: typescript ? 'typescript' : 'javascript', signal: 'package.json', why: null };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const treeStateMemo = new Map ();
|
|
26
|
+
|
|
27
|
+
function clearTreeStateCache() {
|
|
28
|
+
treeStateMemo.clear();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Finds the top of the git work tree and root's relative prefix below it using filesystem reads only.
|
|
33
|
+
*/
|
|
34
|
+
function findWorkTreeTop(root ) {
|
|
35
|
+
let current = path.resolve(root);
|
|
36
|
+
while (true) {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(path.join(current, '.git'))) {
|
|
39
|
+
const rel = path.relative(current, path.resolve(root));
|
|
40
|
+
const prefix = rel && rel !== '.' ? (rel.split(path.sep).join('/') + '/') : '';
|
|
41
|
+
return { top: current, prefix };
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
const parent = path.dirname(current);
|
|
47
|
+
if (parent === current) break;
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
return { top: null, prefix: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function defaultGitRunner(args , cwd ) {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync('git', args, {
|
|
56
|
+
cwd,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
timeout: 2000,
|
|
59
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Measures the repository's HEAD commit, dirty paths and tree fingerprint with one git status call.
|
|
69
|
+
*/
|
|
70
|
+
function measureTreeState(root , runner ) {
|
|
71
|
+
const absRoot = path.resolve(root);
|
|
72
|
+
if (!runner && treeStateMemo.has(absRoot)) {
|
|
73
|
+
return treeStateMemo.get(absRoot) ;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { top: workTreeTop, prefix } = findWorkTreeTop(absRoot);
|
|
77
|
+
if (!workTreeTop) {
|
|
78
|
+
const unmeasured = {
|
|
79
|
+
gitCommit: null,
|
|
80
|
+
dirty: null,
|
|
81
|
+
prefix: null,
|
|
82
|
+
fingerprint: null,
|
|
83
|
+
};
|
|
84
|
+
if (!runner) treeStateMemo.set(absRoot, unmeasured);
|
|
85
|
+
return unmeasured;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const args = ['-C', absRoot, 'status', '--porcelain=v2', '--branch', '-z', '--untracked-files=all', '--ignored=matching'];
|
|
89
|
+
const raw = runner ? runner(args, absRoot) : defaultGitRunner(args, absRoot);
|
|
90
|
+
|
|
91
|
+
if (raw === null || raw === undefined) {
|
|
92
|
+
const commit = runner
|
|
93
|
+
? runner(['-C', absRoot, 'rev-parse', 'HEAD'], absRoot)
|
|
94
|
+
: defaultGitRunner(['-C', absRoot, 'rev-parse', 'HEAD'], absRoot);
|
|
95
|
+
const failed = {
|
|
96
|
+
gitCommit: commit?.trim() || null,
|
|
97
|
+
dirty: null,
|
|
98
|
+
prefix,
|
|
99
|
+
fingerprint: null,
|
|
100
|
+
};
|
|
101
|
+
if (!runner) treeStateMemo.set(absRoot, failed);
|
|
102
|
+
return failed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tokens = raw.split('\0');
|
|
106
|
+
let gitCommit = null;
|
|
107
|
+
const dirty = new Set ();
|
|
108
|
+
const entryRecords = [];
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
111
|
+
const token = tokens[i];
|
|
112
|
+
if (!token) continue;
|
|
113
|
+
|
|
114
|
+
if (token.startsWith('# branch.oid ')) {
|
|
115
|
+
const oid = token.slice('# branch.oid '.length).trim();
|
|
116
|
+
if (oid && oid !== '(initial)') {
|
|
117
|
+
gitCommit = oid;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (token.startsWith('# ')) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (token.startsWith('! ')) {
|
|
126
|
+
const ignoredPath = token.slice(2);
|
|
127
|
+
if (prefix && (ignoredPath === prefix.slice(0, -1) || ignoredPath === `${prefix.slice(0, -1)}/`)) {
|
|
128
|
+
const ignored = { gitCommit, dirty: null, prefix, fingerprint: null };
|
|
129
|
+
if (!runner) treeStateMemo.set(absRoot, ignored);
|
|
130
|
+
return ignored;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let dirtyPath = null;
|
|
136
|
+
let entryText = token;
|
|
137
|
+
|
|
138
|
+
if (token.startsWith('1 ')) {
|
|
139
|
+
const parts = token.split(' ');
|
|
140
|
+
dirtyPath = parts.slice(8).join(' ');
|
|
141
|
+
dirty.add(dirtyPath);
|
|
142
|
+
} else if (token.startsWith('2 ')) {
|
|
143
|
+
const parts = token.split(' ');
|
|
144
|
+
dirtyPath = parts.slice(9).join(' ');
|
|
145
|
+
dirty.add(dirtyPath);
|
|
146
|
+
if (i + 1 < tokens.length && tokens[i + 1]) {
|
|
147
|
+
const origPath = tokens[i + 1];
|
|
148
|
+
dirty.add(origPath);
|
|
149
|
+
entryText = `${token}\0${origPath}`;
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
} else if (token.startsWith('u ')) {
|
|
153
|
+
const parts = token.split(' ');
|
|
154
|
+
dirtyPath = parts.slice(10).join(' ');
|
|
155
|
+
dirty.add(dirtyPath);
|
|
156
|
+
} else if (token.startsWith('? ')) {
|
|
157
|
+
dirtyPath = token.slice(2);
|
|
158
|
+
dirty.add(dirtyPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (dirtyPath !== null) {
|
|
162
|
+
const statePath = `${prefix}.doflow/state/`;
|
|
163
|
+
const normalizedDirtyPath = dirtyPath.split(path.sep).join('/');
|
|
164
|
+
const shouldFingerprint = !normalizedDirtyPath.startsWith(statePath);
|
|
165
|
+
if (shouldFingerprint && /^(?:1|2|u) \S+ S\.M /.test(token)) {
|
|
166
|
+
const nestedRoot = path.resolve(workTreeTop, dirtyPath);
|
|
167
|
+
const nested = measureNestedRepository(nestedRoot);
|
|
168
|
+
if (nested === null) {
|
|
169
|
+
const unknown = { gitCommit, dirty: null, prefix, fingerprint: null };
|
|
170
|
+
if (!runner) treeStateMemo.set(absRoot, unknown);
|
|
171
|
+
return unknown;
|
|
172
|
+
}
|
|
173
|
+
entryRecords.push(`${entryText}:nested:${nested}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (shouldFingerprint && token.startsWith('? ')) {
|
|
177
|
+
const nestedRoot = path.resolve(workTreeTop, dirtyPath.replace(/\/$/, ''));
|
|
178
|
+
if (fs.existsSync(path.join(nestedRoot, '.git'))) {
|
|
179
|
+
const nested = measureNestedRepository(nestedRoot);
|
|
180
|
+
if (nested === null) {
|
|
181
|
+
const unknown = { gitCommit, dirty: null, prefix, fingerprint: null };
|
|
182
|
+
if (!runner) treeStateMemo.set(absRoot, unknown);
|
|
183
|
+
return unknown;
|
|
184
|
+
}
|
|
185
|
+
entryRecords.push(`${entryText}:nested:${nested}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!shouldFingerprint) continue;
|
|
190
|
+
let statTerm = 'absent';
|
|
191
|
+
try {
|
|
192
|
+
const absPath = path.resolve(workTreeTop, dirtyPath);
|
|
193
|
+
const st = fs.statSync(absPath);
|
|
194
|
+
statTerm = `${st.size}:${st.mtimeMs}`;
|
|
195
|
+
} catch {
|
|
196
|
+
statTerm = 'absent';
|
|
197
|
+
}
|
|
198
|
+
entryRecords.push(`${entryText}:${statTerm}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!gitCommit) {
|
|
203
|
+
const uncommitted = {
|
|
204
|
+
gitCommit: null,
|
|
205
|
+
dirty: null,
|
|
206
|
+
prefix,
|
|
207
|
+
fingerprint: null,
|
|
208
|
+
};
|
|
209
|
+
if (!runner) treeStateMemo.set(absRoot, uncommitted);
|
|
210
|
+
return uncommitted;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const hash = crypto.createHash('sha256');
|
|
214
|
+
hash.update(gitCommit);
|
|
215
|
+
hash.update('\0');
|
|
216
|
+
hash.update(entryRecords.sort().join('\0'));
|
|
217
|
+
const fingerprint = `sha256:${hash.digest('hex')}`;
|
|
218
|
+
|
|
219
|
+
const state = {
|
|
220
|
+
gitCommit,
|
|
221
|
+
dirty,
|
|
222
|
+
prefix,
|
|
223
|
+
fingerprint,
|
|
224
|
+
};
|
|
225
|
+
if (!runner) treeStateMemo.set(absRoot, state);
|
|
226
|
+
return state;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function measureNestedRepository(root ) {
|
|
230
|
+
try {
|
|
231
|
+
const gitEntry = path.join(root, '.git');
|
|
232
|
+
const gitStat = fs.statSync(gitEntry);
|
|
233
|
+
const gitDir = gitStat.isFile()
|
|
234
|
+
? path.resolve(root, fs.readFileSync(gitEntry, 'utf8').trim().replace(/^gitdir:\s*/, ''))
|
|
235
|
+
: gitEntry;
|
|
236
|
+
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
|
|
237
|
+
const headRef = head.startsWith('ref: ')
|
|
238
|
+
? fs.readFileSync(path.join(gitDir, head.slice('ref: '.length)), 'utf8').trim()
|
|
239
|
+
: head;
|
|
240
|
+
const index = fs.statSync(path.join(gitDir, 'index'));
|
|
241
|
+
return `${head}:${headRef}:${index.mtimeMs}:${index.size}`;
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function fileHash(root , locator ) {
|
|
248
|
+
if (!locator?.file) return null;
|
|
249
|
+
try {
|
|
250
|
+
const absolute = path.resolve(root, locator.file);
|
|
251
|
+
if (!fs.statSync(absolute).isFile()) return null;
|
|
252
|
+
return `sha256:${crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')}`;
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Derives source version { gitCommit, fileHash, isDirty } for a given locator and tree state.
|
|
260
|
+
*/
|
|
261
|
+
function sourceVersionFor(root , locator , state ) {
|
|
262
|
+
const hash = fileHash(root, locator);
|
|
263
|
+
if (!locator?.file || !state || !state.dirty) {
|
|
264
|
+
return {
|
|
265
|
+
gitCommit: state?.gitCommit ?? null,
|
|
266
|
+
fileHash: hash,
|
|
267
|
+
isDirty: null,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const prefix = state.prefix || '';
|
|
272
|
+
if (path.isAbsolute(locator.file)) {
|
|
273
|
+
return { gitCommit: state.gitCommit ?? null, fileHash: hash, isDirty: null };
|
|
274
|
+
}
|
|
275
|
+
const candidate = path.posix.normalize(`${prefix}${locator.file.split(path.sep).join('/')}`).replace(/^\.\//, '');
|
|
276
|
+
const isDirty = state.dirty.has(candidate);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
gitCommit: state.gitCommit ?? null,
|
|
280
|
+
fileHash: hash,
|
|
281
|
+
isDirty,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
measureTreeState,
|
|
287
|
+
sourceVersionFor,
|
|
288
|
+
clearTreeStateCache,
|
|
289
|
+
};
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
|
-
const { loadStorageRegistry } = require('./registry.
|
|
8
|
-
const { resolveRoots, readPruneSetting } = require('./roots.
|
|
9
|
-
const { runPass } = require('./engine.
|
|
10
|
-
const { upkeepAfterVerb } = require('./upkeep.
|
|
11
|
-
const { REPO_ROOT } = require('../../helper/repo-root.
|
|
7
|
+
const { loadStorageRegistry } = require('./registry.js');
|
|
8
|
+
const { resolveRoots, readPruneSetting } = require('./roots.js');
|
|
9
|
+
const { runPass } = require('./engine.js');
|
|
10
|
+
const { upkeepAfterVerb } = require('./upkeep.js');
|
|
11
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
12
12
|
|
|
13
|
-
function handleStorageCommand({ action = 'report', json = false, global = false, projectRoot = '.', env = process.env, fsImpl = fs }
|
|
13
|
+
function handleStorageCommand({ action = 'report', json = false, global = false, projectRoot = '.', env = process.env, fsImpl = fs } = {}) {
|
|
14
14
|
if (!['report', 'prune', 'upkeep'].includes(action)) {
|
|
15
15
|
console.error(`doflow storage: unknown action '${action}' (expected report, prune, or upkeep)`);
|
|
16
16
|
process.exitCode = 2;
|
|
@@ -23,7 +23,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
|
|
|
23
23
|
}
|
|
24
24
|
let registry;
|
|
25
25
|
try { registry = loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl }); }
|
|
26
|
-
catch (error
|
|
26
|
+
catch (error ) {
|
|
27
27
|
console.error(`doflow storage: storage registry unavailable${error.message ? `: ${error.message}` : ''}`);
|
|
28
28
|
process.exitCode = 2;
|
|
29
29
|
return;
|
|
@@ -35,7 +35,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
|
|
|
35
35
|
readPruneSetting({ registry, roots, rootId, fsImpl })]));
|
|
36
36
|
const mode = action === 'prune' ? 'prune' : 'report';
|
|
37
37
|
const result = runPass({ registry, roots, mode, setting, now: Date.now(), env, fsImpl });
|
|
38
|
-
const removed = result.stores.reduce((sum
|
|
38
|
+
const removed = result.stores.reduce((sum , row ) => sum + row.removed, 0);
|
|
39
39
|
const entries = `entr${removed === 1 ? 'y' : 'ies'}`;
|
|
40
40
|
if (action === 'prune' && Object.values(setting).every(value => value.setting === 'off')) {
|
|
41
41
|
const decision = projectSetting.decidedBy || userSetting.decidedBy || path.join(roots['config-home'], registry.settings.optOutFile);
|
|
@@ -54,7 +54,7 @@ function handleStorageCommand({ action = 'report', json = false, global = false,
|
|
|
54
54
|
}
|
|
55
55
|
console.log(`Settings: project ${projectSetting.setting}${projectSetting.decidedBy ? ` (${projectSetting.decidedBy})` : ' (default)'}; user ${userSetting.setting}${userSetting.decidedBy ? ` (${userSetting.decidedBy})` : ' (default)'}`);
|
|
56
56
|
for (const row of result.stores) {
|
|
57
|
-
const names = row.selected.slice(0, 20).map((entry
|
|
57
|
+
const names = row.selected.slice(0, 20).map((entry ) => entry.name);
|
|
58
58
|
const more = row.selected.length > 20 ? `\n ... and ${row.selected.length - 20} more` : '';
|
|
59
59
|
console.log(`${row.id}\n Location: ${row.location || '(not configured)'}\n Entries: ${row.entries}; size: ${row.bytes} bytes\n Bound: ${row.bound}\n Would remove: ${row.selected.length}${names.length ? `\n ${names.join('\n ')}` : ''}${more}`);
|
|
60
60
|
}
|