specpi 0.10.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -150
- package/LICENSE +21 -21
- package/NPM_RELEASE.md +110 -110
- package/README.md +223 -155
- package/SECURITY.md +86 -85
- package/SECURITY_MODEL.md +123 -107
- package/THIRD_PARTY.md +61 -61
- package/browser-runtime/package-lock.json +86 -86
- package/browser-runtime/package.json +15 -15
- package/extensions/command-guard/powershell-parser.ps1 +47 -47
- package/extensions/spec.ts +236 -10
- package/extensions/tool-wishlist/capabilities.json +114 -114
- package/extensions/tool-wishlist/core.mjs +285 -24
- package/extensions/tool-wishlist/index.ts +939 -90
- package/extensions/tool-wishlist/verification.mjs +810 -0
- package/extensions/workflow-controls/challenge.mjs +139 -13
- package/extensions/workflow-controls/index.ts +811 -35
- package/extensions/workflow-controls/scope.mjs +41 -5
- package/extensions/workflow-controls/smoke.mjs +55 -0
- package/extensions/workflow-controls/task-contract.mjs +439 -0
- package/package.json +98 -98
- package/scripts/check-package.mjs +3 -0
- package/scripts/check-pi-package.mjs +103 -4
- package/scripts/pi-test-harness.mjs +309 -0
- package/scripts/specpi.mjs +31 -2
- package/skills/specpi-improve/SKILL.md +60 -54
- package/specpi +0 -0
- package/templates/AGENTS.md +25 -23
- package/templates/settings.json +10 -10
- package/themes/specpi-spec.json +96 -96
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "specpi-browser-runtime",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "specpi-browser-runtime",
|
|
9
|
-
"version": "0.1.0",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"pixelmatch": "7.2.0",
|
|
13
|
-
"playwright": "1.62.1",
|
|
14
|
-
"pngjs": "7.0.0"
|
|
15
|
-
},
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=22.19.0"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"node_modules/fsevents": {
|
|
21
|
-
"version": "2.3.2",
|
|
22
|
-
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
23
|
-
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
24
|
-
"hasInstallScript": true,
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"optional": true,
|
|
27
|
-
"os": [
|
|
28
|
-
"darwin"
|
|
29
|
-
],
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"node_modules/pixelmatch": {
|
|
35
|
-
"version": "7.2.0",
|
|
36
|
-
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.2.0.tgz",
|
|
37
|
-
"integrity": "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==",
|
|
38
|
-
"license": "ISC",
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"pngjs": "^7.0.0"
|
|
41
|
-
},
|
|
42
|
-
"bin": {
|
|
43
|
-
"pixelmatch": "bin/pixelmatch"
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"node_modules/playwright": {
|
|
47
|
-
"version": "1.62.1",
|
|
48
|
-
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
|
49
|
-
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
|
50
|
-
"license": "Apache-2.0",
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"playwright-core": "1.62.1"
|
|
53
|
-
},
|
|
54
|
-
"bin": {
|
|
55
|
-
"playwright": "cli.js"
|
|
56
|
-
},
|
|
57
|
-
"engines": {
|
|
58
|
-
"node": ">=20"
|
|
59
|
-
},
|
|
60
|
-
"optionalDependencies": {
|
|
61
|
-
"fsevents": "2.3.2"
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
"node_modules/playwright-core": {
|
|
65
|
-
"version": "1.62.1",
|
|
66
|
-
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
|
67
|
-
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
|
68
|
-
"license": "Apache-2.0",
|
|
69
|
-
"bin": {
|
|
70
|
-
"playwright-core": "cli.js"
|
|
71
|
-
},
|
|
72
|
-
"engines": {
|
|
73
|
-
"node": ">=20"
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
"node_modules/pngjs": {
|
|
77
|
-
"version": "7.0.0",
|
|
78
|
-
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
|
79
|
-
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
|
80
|
-
"license": "MIT",
|
|
81
|
-
"engines": {
|
|
82
|
-
"node": ">=14.19.0"
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "specpi-browser-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "specpi-browser-runtime",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"pixelmatch": "7.2.0",
|
|
13
|
+
"playwright": "1.62.1",
|
|
14
|
+
"pngjs": "7.0.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.19.0"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"node_modules/fsevents": {
|
|
21
|
+
"version": "2.3.2",
|
|
22
|
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
23
|
+
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
24
|
+
"hasInstallScript": true,
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"optional": true,
|
|
27
|
+
"os": [
|
|
28
|
+
"darwin"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"node_modules/pixelmatch": {
|
|
35
|
+
"version": "7.2.0",
|
|
36
|
+
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.2.0.tgz",
|
|
37
|
+
"integrity": "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==",
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"pngjs": "^7.0.0"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"pixelmatch": "bin/pixelmatch"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"node_modules/playwright": {
|
|
47
|
+
"version": "1.62.1",
|
|
48
|
+
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
|
49
|
+
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
|
50
|
+
"license": "Apache-2.0",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"playwright-core": "1.62.1"
|
|
53
|
+
},
|
|
54
|
+
"bin": {
|
|
55
|
+
"playwright": "cli.js"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
},
|
|
60
|
+
"optionalDependencies": {
|
|
61
|
+
"fsevents": "2.3.2"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"node_modules/playwright-core": {
|
|
65
|
+
"version": "1.62.1",
|
|
66
|
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
|
67
|
+
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
|
68
|
+
"license": "Apache-2.0",
|
|
69
|
+
"bin": {
|
|
70
|
+
"playwright-core": "cli.js"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=20"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"node_modules/pngjs": {
|
|
77
|
+
"version": "7.0.0",
|
|
78
|
+
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
|
79
|
+
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
|
80
|
+
"license": "MIT",
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=14.19.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "specpi-browser-runtime",
|
|
3
|
-
"private": true,
|
|
4
|
-
"version": "0.1.0",
|
|
5
|
-
"description": "Pinned local browser runtime managed by the SpecPi installer",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"pixelmatch": "7.2.0",
|
|
9
|
-
"playwright": "1.62.1",
|
|
10
|
-
"pngjs": "7.0.0"
|
|
11
|
-
},
|
|
12
|
-
"engines": {
|
|
13
|
-
"node": ">=22.19.0"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "specpi-browser-runtime",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Pinned local browser runtime managed by the SpecPi installer",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"pixelmatch": "7.2.0",
|
|
9
|
+
"playwright": "1.62.1",
|
|
10
|
+
"pngjs": "7.0.0"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22.19.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
$ErrorActionPreference = 'Stop'
|
|
2
|
-
$maxInput = 131072
|
|
3
|
-
$maxTokens = 4096
|
|
4
|
-
$maxCommands = 128
|
|
5
|
-
$maxLiteral = 65536
|
|
6
|
-
$text = [Console]::In.ReadToEnd()
|
|
7
|
-
$edition = if ($PSVersionTable.PSEdition -eq 'Desktop') { 'Desktop' } else { 'Core' }
|
|
8
|
-
function Clip([string]$s, [int]$n = 512) { if ($null -eq $s) { return $null }; if ($s.Length -gt $n) { return $s.Substring(0, $n) }; return $s }
|
|
9
|
-
function Pos($e) { if ($null -eq $e) { return @{ start = 0; end = 0 } }; return @{ start = [int]$e.StartOffset; end = [int]$e.EndOffset } }
|
|
10
|
-
function DynamicNode($n) { return ($n -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -or $n -is [System.Management.Automation.Language.SubExpressionAst] -or $n -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -or $n -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) }
|
|
11
|
-
try {
|
|
12
|
-
if ($text.Length -gt $maxInput) { throw 'input limit exceeded' }
|
|
13
|
-
$tokens = $null; $errors = $null
|
|
14
|
-
$ast = [System.Management.Automation.Language.Parser]::ParseInput($text, [ref]$tokens, [ref]$errors)
|
|
15
|
-
$errorItems = @($errors | Select-Object -First 32 | ForEach-Object { $p = Pos $_.Extent; @{ errorId = Clip $_.ErrorId 96; start = $p.start; end = $p.end; message = Clip $_.Message 256 } })
|
|
16
|
-
$commands = @($ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true) | ForEach-Object {
|
|
17
|
-
$command = $_; $p = Pos $command.Extent; $name = $command.GetCommandName()
|
|
18
|
-
$pipeline = $command.Parent; while ($null -ne $pipeline -and -not ($pipeline -is [System.Management.Automation.Language.PipelineAst])) { $pipeline = $pipeline.Parent }
|
|
19
|
-
$pipelineStart = if ($null -ne $pipeline) { [int]$pipeline.Extent.StartOffset } else { $null }
|
|
20
|
-
$elements = @($command.CommandElements | Select-Object -First 256 | ForEach-Object {
|
|
21
|
-
$ep = Pos $_.Extent; $literal = $null
|
|
22
|
-
$literalTruncated = $false; $elementDynamic = [bool](DynamicNode $_)
|
|
23
|
-
if ($_ -is [System.Management.Automation.Language.CommandParameterAst]) {
|
|
24
|
-
$rawLiteral = '-' + $_.ParameterName
|
|
25
|
-
if ($null -ne $_.Argument) {
|
|
26
|
-
if ($_.Argument -is [System.Management.Automation.Language.StringConstantExpressionAst] -or $_.Argument -is [System.Management.Automation.Language.ConstantExpressionAst]) { $rawLiteral += ':' + [string]$_.Argument.Value }
|
|
27
|
-
else { $elementDynamic = $true }
|
|
28
|
-
}
|
|
29
|
-
$literalTruncated = $rawLiteral.Length -gt $maxLiteral; $literal = Clip $rawLiteral $maxLiteral
|
|
30
|
-
} elseif ($_ -is [System.Management.Automation.Language.StringConstantExpressionAst] -or $_ -is [System.Management.Automation.Language.ConstantExpressionAst]) { $rawLiteral = [string]$_.Value; $literalTruncated = $rawLiteral.Length -gt $maxLiteral; $literal = Clip $rawLiteral $maxLiteral }
|
|
31
|
-
$rawExtent = [string]$_.Extent.Text; $rawTruncated = $rawExtent.Length -gt $maxLiteral
|
|
32
|
-
@{ astType = $_.GetType().Name; start = $ep.start; end = $ep.end; literal = $literal; literalTruncated = $literalTruncated; raw = (Clip $rawExtent $maxLiteral); rawTruncated = $rawTruncated; dynamic = $elementDynamic }
|
|
33
|
-
})
|
|
34
|
-
$reds = @($command.Redirections | Select-Object -First 128 | ForEach-Object { $rp = Pos $_.Extent; $target = $null; $targetTruncated = $false; if ($_.Location -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $rawTarget = [string]$_.Location.Value; $target = Clip $rawTarget 4096; $targetTruncated = $rawTarget.Length -gt 4096 }; @{ astType = $_.GetType().Name; start = $rp.start; end = $rp.end; targetLiteral = $target; targetTruncated = [bool]$targetTruncated; dynamic = $null -eq $target } })
|
|
35
|
-
@{ start = $p.start; end = $p.end; pipelineStart = $pipelineStart; commandName = $name; invocationOperator = ([string]$command.InvocationOperator); elements = $elements; elementsTruncated = ($command.CommandElements.Count -gt 256); redirections = $reds; redirectionsTruncated = ($command.Redirections.Count -gt 128) }
|
|
36
|
-
})
|
|
37
|
-
$dynamic = @($ast.FindAll({ param($n) DynamicNode $n }, $true) | Select-Object -First 256 | ForEach-Object { $p = Pos $_.Extent; @{ kind = $_.GetType().Name; start = $p.start; end = $p.end } })
|
|
38
|
-
$stop = @($tokens | Where-Object { ([string]$_.Kind -eq 'StopParsing') -or $_.Text -eq '--%' } | Select-Object -First 128 | ForEach-Object { $p = Pos $_.Extent; @{ start = $p.start; end = $p.end } })
|
|
39
|
-
$limit = $null; if ($tokens.Count -gt $maxTokens) { $limit = 'tokens' }; if ($commands.Count -gt $maxCommands) { $limit = 'commands' }; if (@($commands | Where-Object { $_.elementsTruncated }).Count -gt 0) { $limit = 'elements' }; if (@($commands | Where-Object { $_.redirectionsTruncated }).Count -gt 0) { $limit = 'redirections' }
|
|
40
|
-
$out = @{ schema = 1; ok = ($errorItems.Count -eq 0 -and $null -eq $limit); parser = @{ edition = $edition; version = [string]$PSVersionTable.PSVersion }; tokenCount = [int]$tokens.Count; errors = $errorItems; commands = @($commands | Select-Object -First $maxCommands); dynamicConstructs = $dynamic; stopParsingTokens = $stop }
|
|
41
|
-
if ($null -ne $limit) { $out.limitExceeded = $limit }
|
|
42
|
-
[Console]::Out.Write((ConvertTo-Json $out -Compress -Depth 16))
|
|
43
|
-
} catch {
|
|
44
|
-
$out = @{ schema = 1; ok = $false; parser = @{ edition = $edition; version = [string]$PSVersionTable.PSVersion }; tokenCount = 0; errors = @(@{ errorId = 'GUARD_HELPER_FAILURE'; start = 0; end = 0; message = (Clip $_.Exception.Message 256) }); commands = @(); dynamicConstructs = @(); stopParsingTokens = @() }
|
|
45
|
-
[Console]::Out.Write((ConvertTo-Json $out -Compress -Depth 16))
|
|
46
|
-
exit 2
|
|
47
|
-
}
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
$maxInput = 131072
|
|
3
|
+
$maxTokens = 4096
|
|
4
|
+
$maxCommands = 128
|
|
5
|
+
$maxLiteral = 65536
|
|
6
|
+
$text = [Console]::In.ReadToEnd()
|
|
7
|
+
$edition = if ($PSVersionTable.PSEdition -eq 'Desktop') { 'Desktop' } else { 'Core' }
|
|
8
|
+
function Clip([string]$s, [int]$n = 512) { if ($null -eq $s) { return $null }; if ($s.Length -gt $n) { return $s.Substring(0, $n) }; return $s }
|
|
9
|
+
function Pos($e) { if ($null -eq $e) { return @{ start = 0; end = 0 } }; return @{ start = [int]$e.StartOffset; end = [int]$e.EndOffset } }
|
|
10
|
+
function DynamicNode($n) { return ($n -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -or $n -is [System.Management.Automation.Language.SubExpressionAst] -or $n -is [System.Management.Automation.Language.InvokeMemberExpressionAst] -or $n -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) }
|
|
11
|
+
try {
|
|
12
|
+
if ($text.Length -gt $maxInput) { throw 'input limit exceeded' }
|
|
13
|
+
$tokens = $null; $errors = $null
|
|
14
|
+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($text, [ref]$tokens, [ref]$errors)
|
|
15
|
+
$errorItems = @($errors | Select-Object -First 32 | ForEach-Object { $p = Pos $_.Extent; @{ errorId = Clip $_.ErrorId 96; start = $p.start; end = $p.end; message = Clip $_.Message 256 } })
|
|
16
|
+
$commands = @($ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true) | ForEach-Object {
|
|
17
|
+
$command = $_; $p = Pos $command.Extent; $name = $command.GetCommandName()
|
|
18
|
+
$pipeline = $command.Parent; while ($null -ne $pipeline -and -not ($pipeline -is [System.Management.Automation.Language.PipelineAst])) { $pipeline = $pipeline.Parent }
|
|
19
|
+
$pipelineStart = if ($null -ne $pipeline) { [int]$pipeline.Extent.StartOffset } else { $null }
|
|
20
|
+
$elements = @($command.CommandElements | Select-Object -First 256 | ForEach-Object {
|
|
21
|
+
$ep = Pos $_.Extent; $literal = $null
|
|
22
|
+
$literalTruncated = $false; $elementDynamic = [bool](DynamicNode $_)
|
|
23
|
+
if ($_ -is [System.Management.Automation.Language.CommandParameterAst]) {
|
|
24
|
+
$rawLiteral = '-' + $_.ParameterName
|
|
25
|
+
if ($null -ne $_.Argument) {
|
|
26
|
+
if ($_.Argument -is [System.Management.Automation.Language.StringConstantExpressionAst] -or $_.Argument -is [System.Management.Automation.Language.ConstantExpressionAst]) { $rawLiteral += ':' + [string]$_.Argument.Value }
|
|
27
|
+
else { $elementDynamic = $true }
|
|
28
|
+
}
|
|
29
|
+
$literalTruncated = $rawLiteral.Length -gt $maxLiteral; $literal = Clip $rawLiteral $maxLiteral
|
|
30
|
+
} elseif ($_ -is [System.Management.Automation.Language.StringConstantExpressionAst] -or $_ -is [System.Management.Automation.Language.ConstantExpressionAst]) { $rawLiteral = [string]$_.Value; $literalTruncated = $rawLiteral.Length -gt $maxLiteral; $literal = Clip $rawLiteral $maxLiteral }
|
|
31
|
+
$rawExtent = [string]$_.Extent.Text; $rawTruncated = $rawExtent.Length -gt $maxLiteral
|
|
32
|
+
@{ astType = $_.GetType().Name; start = $ep.start; end = $ep.end; literal = $literal; literalTruncated = $literalTruncated; raw = (Clip $rawExtent $maxLiteral); rawTruncated = $rawTruncated; dynamic = $elementDynamic }
|
|
33
|
+
})
|
|
34
|
+
$reds = @($command.Redirections | Select-Object -First 128 | ForEach-Object { $rp = Pos $_.Extent; $target = $null; $targetTruncated = $false; if ($_.Location -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $rawTarget = [string]$_.Location.Value; $target = Clip $rawTarget 4096; $targetTruncated = $rawTarget.Length -gt 4096 }; @{ astType = $_.GetType().Name; start = $rp.start; end = $rp.end; targetLiteral = $target; targetTruncated = [bool]$targetTruncated; dynamic = $null -eq $target } })
|
|
35
|
+
@{ start = $p.start; end = $p.end; pipelineStart = $pipelineStart; commandName = $name; invocationOperator = ([string]$command.InvocationOperator); elements = $elements; elementsTruncated = ($command.CommandElements.Count -gt 256); redirections = $reds; redirectionsTruncated = ($command.Redirections.Count -gt 128) }
|
|
36
|
+
})
|
|
37
|
+
$dynamic = @($ast.FindAll({ param($n) DynamicNode $n }, $true) | Select-Object -First 256 | ForEach-Object { $p = Pos $_.Extent; @{ kind = $_.GetType().Name; start = $p.start; end = $p.end } })
|
|
38
|
+
$stop = @($tokens | Where-Object { ([string]$_.Kind -eq 'StopParsing') -or $_.Text -eq '--%' } | Select-Object -First 128 | ForEach-Object { $p = Pos $_.Extent; @{ start = $p.start; end = $p.end } })
|
|
39
|
+
$limit = $null; if ($tokens.Count -gt $maxTokens) { $limit = 'tokens' }; if ($commands.Count -gt $maxCommands) { $limit = 'commands' }; if (@($commands | Where-Object { $_.elementsTruncated }).Count -gt 0) { $limit = 'elements' }; if (@($commands | Where-Object { $_.redirectionsTruncated }).Count -gt 0) { $limit = 'redirections' }
|
|
40
|
+
$out = @{ schema = 1; ok = ($errorItems.Count -eq 0 -and $null -eq $limit); parser = @{ edition = $edition; version = [string]$PSVersionTable.PSVersion }; tokenCount = [int]$tokens.Count; errors = $errorItems; commands = @($commands | Select-Object -First $maxCommands); dynamicConstructs = $dynamic; stopParsingTokens = $stop }
|
|
41
|
+
if ($null -ne $limit) { $out.limitExceeded = $limit }
|
|
42
|
+
[Console]::Out.Write((ConvertTo-Json $out -Compress -Depth 16))
|
|
43
|
+
} catch {
|
|
44
|
+
$out = @{ schema = 1; ok = $false; parser = @{ edition = $edition; version = [string]$PSVersionTable.PSVersion }; tokenCount = 0; errors = @(@{ errorId = 'GUARD_HELPER_FAILURE'; start = 0; end = 0; message = (Clip $_.Exception.Message 256) }); commands = @(); dynamicConstructs = @(); stopParsingTokens = @() }
|
|
45
|
+
[Console]::Out.Write((ConvertTo-Json $out -Compress -Depth 16))
|
|
46
|
+
exit 2
|
|
47
|
+
}
|
package/extensions/spec.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { createHash } from "node:crypto";
|
|
|
18
18
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { Key, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
20
20
|
import { describeSpecPhase, transformSpecMarkdown } from "./spec/core.mjs";
|
|
21
|
+
import { canonicalRoot } from "./workflow-controls/scope.mjs";
|
|
22
|
+
import { readTaskContract } from "./workflow-controls/task-contract.mjs";
|
|
21
23
|
|
|
22
24
|
const specPiAgentDir = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
|
|
23
25
|
const specPiStateDir = path.join(specPiAgentDir, "specpi");
|
|
@@ -68,7 +70,17 @@ if (hasValidatedSpecPiTools()) {
|
|
|
68
70
|
|
|
69
71
|
type SpecPhase = "ready" | "thinking" | "reasoning" | "synthesizing" | `using ${string}` | `${string} failed`;
|
|
70
72
|
|
|
73
|
+
interface SpecTaskState {
|
|
74
|
+
active: boolean;
|
|
75
|
+
objective: string;
|
|
76
|
+
proven: number;
|
|
77
|
+
total: number;
|
|
78
|
+
digest?: string;
|
|
79
|
+
error?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
const SPEC_STATE_ENTRY = "spec-mode";
|
|
83
|
+
const CHALLENGE_ENTRY = "specpi-completion-challenge";
|
|
72
84
|
const SPEC_SYSTEM_GUIDANCE = `
|
|
73
85
|
|
|
74
86
|
[SPEC MODE — SPEC EXECUTION]
|
|
@@ -118,13 +130,164 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
130
|
let phase: SpecPhase = "ready";
|
|
119
131
|
let turnCount = 0;
|
|
120
132
|
let toolCount = 0;
|
|
121
|
-
let workflowScope: {
|
|
133
|
+
let workflowScope: {
|
|
134
|
+
active: boolean;
|
|
135
|
+
pending: number;
|
|
136
|
+
indeterminate: boolean;
|
|
137
|
+
taskBound: boolean;
|
|
138
|
+
taskStale: boolean;
|
|
139
|
+
} = {
|
|
122
140
|
active: false,
|
|
123
141
|
pending: 0,
|
|
124
142
|
indeterminate: false,
|
|
143
|
+
taskBound: false,
|
|
144
|
+
taskStale: false,
|
|
125
145
|
};
|
|
146
|
+
let workflowTask: SpecTaskState = {
|
|
147
|
+
active: false,
|
|
148
|
+
objective: "",
|
|
149
|
+
proven: 0,
|
|
150
|
+
total: 0,
|
|
151
|
+
};
|
|
152
|
+
let taskRefreshPending = true;
|
|
153
|
+
let taskRefreshGeneration = 0;
|
|
126
154
|
let toolsWereExpanded: boolean | undefined;
|
|
127
155
|
let requestSpecRender: (() => void) | undefined;
|
|
156
|
+
let lastContext: ExtensionContext | undefined;
|
|
157
|
+
|
|
158
|
+
const resolveRoot = async (cwd: string) => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd, timeout: 15_000 });
|
|
161
|
+
if (result.code === 0 && typeof result.stdout === "string" && result.stdout.trim()) {
|
|
162
|
+
return canonicalRoot(path.resolve(cwd, result.stdout.trim()));
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
/* A non-Git session can still display a task contract rooted at its current directory. */
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return canonicalRoot(cwd);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const refreshTask = async (ctx: ExtensionContext, expectedGeneration = taskRefreshGeneration) => {
|
|
172
|
+
const cwd = ctx.cwd;
|
|
173
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
174
|
+
const isCurrent = () =>
|
|
175
|
+
expectedGeneration === taskRefreshGeneration &&
|
|
176
|
+
cwd === ctx.cwd &&
|
|
177
|
+
sessionId === ctx.sessionManager.getSessionId();
|
|
178
|
+
const root = await resolveRoot(cwd);
|
|
179
|
+
if (!isCurrent()) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let contract;
|
|
184
|
+
try {
|
|
185
|
+
contract = readTaskContract(ctx.sessionManager.getBranch?.() ?? [], root);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!isCurrent()) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
workflowTask = {
|
|
192
|
+
active: false,
|
|
193
|
+
objective: "",
|
|
194
|
+
proven: 0,
|
|
195
|
+
total: 0,
|
|
196
|
+
error:
|
|
197
|
+
error instanceof Error
|
|
198
|
+
? error.message.replace(/[\u0000-\u001f\u007f]+/gu, " ").slice(0, 160)
|
|
199
|
+
: "unavailable",
|
|
200
|
+
};
|
|
201
|
+
taskRefreshPending = false;
|
|
202
|
+
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!isCurrent()) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!contract) {
|
|
211
|
+
workflowTask = { active: false, objective: "", proven: 0, total: 0 };
|
|
212
|
+
taskRefreshPending = false;
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let latestReview;
|
|
218
|
+
for (const entry of ctx.sessionManager.getBranch?.() ?? []) {
|
|
219
|
+
if (entry.type !== "custom" || entry.customType !== CHALLENGE_ENTRY) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (entry.data?.kind === "result") {
|
|
224
|
+
latestReview = entry.data;
|
|
225
|
+
} else if (entry.data?.kind === "cleared") {
|
|
226
|
+
latestReview = undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (latestReview?.facts?.taskContractDigest !== contract.digest) {
|
|
231
|
+
latestReview = undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const provenIds = new Set(
|
|
235
|
+
latestReview?.result?.requirements
|
|
236
|
+
?.filter((item: any) => item?.status === "proven" && typeof item.id === "string")
|
|
237
|
+
.map((item: any) => item.id) ?? [],
|
|
238
|
+
);
|
|
239
|
+
workflowTask = {
|
|
240
|
+
active: true,
|
|
241
|
+
objective: contract.objective,
|
|
242
|
+
proven: contract.requirements.filter((item: any) => provenIds.has(item.id)).length,
|
|
243
|
+
total: contract.requirements.length,
|
|
244
|
+
digest: contract.digest,
|
|
245
|
+
};
|
|
246
|
+
taskRefreshPending = false;
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const queueTaskRefresh = (ctx = lastContext) => {
|
|
252
|
+
taskRefreshPending = true;
|
|
253
|
+
const generation = ++taskRefreshGeneration;
|
|
254
|
+
if (!ctx) {
|
|
255
|
+
requestSpecRender?.();
|
|
256
|
+
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
void refreshTask(ctx, generation)
|
|
261
|
+
.then((applied) => {
|
|
262
|
+
if (applied) {
|
|
263
|
+
requestSpecRender?.();
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
.catch(() => {
|
|
267
|
+
if (generation === taskRefreshGeneration) {
|
|
268
|
+
requestSpecRender?.();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
requestSpecRender?.();
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const taskStatusLabel = () => {
|
|
275
|
+
if (workflowTask.error) {
|
|
276
|
+
return "INVALID";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!workflowTask.active) {
|
|
280
|
+
return "UNSET";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return `${workflowTask.proven}/${workflowTask.total}`;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const taskObjectiveLabel = () => {
|
|
287
|
+
const objective = workflowTask.objective.replace(/[\u0000-\u001f\u007f]+/gu, " ").trim();
|
|
288
|
+
|
|
289
|
+
return objective.length > 42 ? `${objective.slice(0, 39)}...` : objective;
|
|
290
|
+
};
|
|
128
291
|
|
|
129
292
|
const updateActivity = (ctx: ExtensionContext) => {
|
|
130
293
|
if (!enabled) {
|
|
@@ -226,15 +389,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
389
|
const run = `${countLabel(turnCount, "T")} · ${countLabel(toolCount, "X")}`;
|
|
227
390
|
const scope = !workflowScope.active
|
|
228
391
|
? "UNSET"
|
|
229
|
-
: workflowScope.pending > 0 || workflowScope.indeterminate
|
|
392
|
+
: workflowScope.pending > 0 || workflowScope.indeterminate || workflowScope.taskStale
|
|
230
393
|
? "REVIEW"
|
|
231
394
|
: "CLEAN";
|
|
395
|
+
const taskStatus = taskStatusLabel();
|
|
396
|
+
const taskObjective = taskObjectiveLabel() || "UNSET";
|
|
232
397
|
|
|
233
398
|
if (width < 44) {
|
|
234
399
|
return [
|
|
235
400
|
renderEdge("┌", "┐", "─ ACTIVE SPEC ", width, (text) => theme.fg("borderAccent", text)),
|
|
236
401
|
truncateToWidth(theme.fg("accent", phaseText), width, ""),
|
|
237
|
-
truncateToWidth(theme.fg("dim", `${run} ·
|
|
402
|
+
truncateToWidth(theme.fg("dim", `${run} · TASK ${taskStatus} · SCOPE ${scope}`), width, ""),
|
|
403
|
+
truncateToWidth(theme.fg("muted", taskObjective), width, ""),
|
|
238
404
|
renderEdge("└", "┘", "─ /spec exits ", width, (text) => theme.fg("borderMuted", text)),
|
|
239
405
|
];
|
|
240
406
|
}
|
|
@@ -252,6 +418,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
252
418
|
theme.fg("muted", "OUTPUT RESPONSE HELD · TOOLS COLLAPSED"),
|
|
253
419
|
theme.fg("dim", `SCOPE ${scope}`),
|
|
254
420
|
),
|
|
421
|
+
row(theme.fg("muted", `TASK ${taskObjective}`), theme.fg("dim", `REQ ${taskStatus}`)),
|
|
255
422
|
renderEdge("└", "┘", "─ CTRL+ALT+Z / EXIT SPEC MODE ", width, (text) =>
|
|
256
423
|
theme.fg("borderMuted", text),
|
|
257
424
|
),
|
|
@@ -310,12 +477,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
310
477
|
|
|
311
478
|
pi.registerMarkdownTransformer((markdown, context) => transformSpecMarkdown(markdown, context, enabled));
|
|
312
479
|
|
|
313
|
-
|
|
480
|
+
const restoreSession = async (ctx: ExtensionContext) => {
|
|
481
|
+
lastContext = ctx;
|
|
482
|
+
const generation = ++taskRefreshGeneration;
|
|
483
|
+
if (enabled) {
|
|
484
|
+
clearMode(ctx);
|
|
485
|
+
}
|
|
486
|
+
|
|
314
487
|
enabled = false;
|
|
315
488
|
phase = "ready";
|
|
316
489
|
turnCount = 0;
|
|
317
490
|
toolCount = 0;
|
|
318
|
-
workflowScope = {
|
|
491
|
+
workflowScope = {
|
|
492
|
+
active: false,
|
|
493
|
+
pending: 0,
|
|
494
|
+
indeterminate: false,
|
|
495
|
+
taskBound: false,
|
|
496
|
+
taskStale: false,
|
|
497
|
+
};
|
|
498
|
+
workflowTask = { active: false, objective: "", proven: 0, total: 0 };
|
|
499
|
+
taskRefreshPending = true;
|
|
319
500
|
toolsWereExpanded = undefined;
|
|
320
501
|
requestSpecRender = undefined;
|
|
321
502
|
|
|
@@ -330,18 +511,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
330
511
|
if (enabled) {
|
|
331
512
|
applyMode(ctx);
|
|
332
513
|
}
|
|
333
|
-
|
|
514
|
+
|
|
515
|
+
if (await refreshTask(ctx, generation)) {
|
|
516
|
+
requestSpecRender?.();
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
pi.on("session_start", (_event, ctx) => restoreSession(ctx));
|
|
521
|
+
pi.on("session_tree", (_event, ctx) => restoreSession(ctx));
|
|
334
522
|
|
|
335
523
|
pi.events.on("specpi:workflow-status", (state: any) => {
|
|
336
524
|
workflowScope = {
|
|
337
525
|
active: state?.active === true,
|
|
338
526
|
pending: Number.isInteger(state?.pending) ? Math.max(0, state.pending) : 0,
|
|
339
527
|
indeterminate: state?.indeterminate === true,
|
|
528
|
+
taskBound: state?.taskBound === true,
|
|
529
|
+
taskStale: state?.taskStale === true,
|
|
340
530
|
};
|
|
341
|
-
|
|
531
|
+
if (state?.taskBound === true || state?.taskReviewChanged === true) {
|
|
532
|
+
// Scope status is also the notification used when a challenge result changes the proven requirement
|
|
533
|
+
// count. Refresh from the branch so the panel follows that result immediately while generation checks
|
|
534
|
+
// prevent a delayed root lookup from overwriting a newer session.
|
|
535
|
+
queueTaskRefresh();
|
|
536
|
+
} else {
|
|
537
|
+
taskRefreshPending = true;
|
|
538
|
+
requestSpecRender?.();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
pi.events.on("specpi:task-contract-changed", () => {
|
|
543
|
+
queueTaskRefresh();
|
|
342
544
|
});
|
|
343
545
|
|
|
344
546
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
547
|
+
taskRefreshGeneration += 1;
|
|
548
|
+
lastContext = undefined;
|
|
345
549
|
if (enabled) {
|
|
346
550
|
clearMode(ctx);
|
|
347
551
|
} else {
|
|
@@ -397,12 +601,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
601
|
updateActivity(ctx);
|
|
398
602
|
});
|
|
399
603
|
|
|
400
|
-
pi.on("before_agent_start", async (event) => {
|
|
604
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
605
|
+
lastContext = ctx;
|
|
606
|
+
if (taskRefreshPending || enabled) {
|
|
607
|
+
const generation = ++taskRefreshGeneration;
|
|
608
|
+
if (!(await refreshTask(ctx, generation))) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
requestSpecRender?.();
|
|
613
|
+
}
|
|
614
|
+
|
|
401
615
|
if (!enabled || event.systemPrompt.includes("[SPEC MODE — SPEC EXECUTION]")) {
|
|
402
616
|
return;
|
|
403
617
|
}
|
|
404
618
|
|
|
405
|
-
|
|
619
|
+
const taskGuidance = workflowTask.active
|
|
620
|
+
? `\n\n[SPEC TASK]\nObjective: ${taskObjectiveLabel()}\nFixed requirements proven: ${workflowTask.proven}/${workflowTask.total}. Assess the original requirement set by its stable IDs.`
|
|
621
|
+
: workflowTask.error
|
|
622
|
+
? `\n\n[SPEC TASK]\nTask contract unavailable: ${workflowTask.error}`
|
|
623
|
+
: "";
|
|
624
|
+
|
|
625
|
+
return { systemPrompt: `${event.systemPrompt}${SPEC_SYSTEM_GUIDANCE}${taskGuidance}` };
|
|
406
626
|
});
|
|
407
627
|
|
|
408
628
|
pi.registerCommand("spec", {
|
|
@@ -414,6 +634,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
414
634
|
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
415
635
|
},
|
|
416
636
|
handler: async (args, ctx) => {
|
|
637
|
+
lastContext = ctx;
|
|
417
638
|
const action = args.trim().toLowerCase();
|
|
418
639
|
if (!action) {
|
|
419
640
|
toggleMode(ctx);
|
|
@@ -434,11 +655,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
434
655
|
}
|
|
435
656
|
|
|
436
657
|
if (action === "status") {
|
|
658
|
+
const generation = ++taskRefreshGeneration;
|
|
659
|
+
if (!(await refreshTask(ctx, generation))) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
437
663
|
const state = describeSpecPhase(phase);
|
|
438
664
|
const detail = state.detail ? ` · ${state.detail}` : "";
|
|
439
665
|
ctx.ui.notify(
|
|
440
666
|
enabled
|
|
441
|
-
? `SPEC MODE / ACTIVE · ${state.index} / ${state.label}${detail} · ${countLabel(turnCount, "T")} · ${countLabel(toolCount, "X")}`
|
|
667
|
+
? `SPEC MODE / ACTIVE · ${state.index} / ${state.label}${detail} · ${countLabel(turnCount, "T")} · ${countLabel(toolCount, "X")} · task: ${taskObjectiveLabel() || "unset"} · requirements: ${taskStatusLabel()}`
|
|
442
668
|
: "Spec mode is off.",
|
|
443
669
|
"info",
|
|
444
670
|
);
|